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
|
@@ -0,0 +1,2818 @@
|
|
|
1
|
+
import { writeErr, writeOut } from "../psql/command/shared.js";
|
|
2
|
+
import { formatDurationMs } from "../psql/print/units.js";
|
|
3
|
+
import { alignedPrinter } from "../psql/print/aligned.js";
|
|
4
|
+
import { asciidocPrinter } from "../psql/print/asciidoc.js";
|
|
5
|
+
import { csvPrinter } from "../psql/print/csv.js";
|
|
6
|
+
import { htmlPrinter } from "../psql/print/html.js";
|
|
7
|
+
import { jsonPrinter } from "../psql/print/json.js";
|
|
8
|
+
import { latexLongtablePrinter, latexPrinter } from "../psql/print/latex.js";
|
|
9
|
+
import { troffMsPrinter } from "../psql/print/troff.js";
|
|
10
|
+
import { unalignedPrinter } from "../psql/print/unaligned.js";
|
|
11
|
+
import { openPager, shouldPage } from "../psql/print/pager.js";
|
|
12
|
+
import { formatErrorReport, psqlErrorPrefix } from "../psql/command/cmd_meta.js";
|
|
13
|
+
import { applyPset } from "../psql/command/cmd_format.js";
|
|
14
|
+
import { closeSync, createWriteStream, fsyncSync, openSync, promises } from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { platform } from "node:os";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
//#region src/psql/core/common.ts
|
|
19
|
+
/**
|
|
20
|
+
* psql common — the unified send-query / process-result pipeline.
|
|
21
|
+
*
|
|
22
|
+
* TypeScript port of selected functions in `src/bin/psql/common.c`:
|
|
23
|
+
*
|
|
24
|
+
* - {@link sendQuery} ← `SendQuery`
|
|
25
|
+
* - {@link executeAndPrint} ← `ExecQueryAndProcessResults` (the inner
|
|
26
|
+
* result-processing slice, without the
|
|
27
|
+
* AUTOCOMMIT / savepoint scaffolding)
|
|
28
|
+
* - {@link psqlExec} ← `PSQLexec`
|
|
29
|
+
*
|
|
30
|
+
* The minimal version of this logic was inlined in `mainloop.ts` after WP-12;
|
|
31
|
+
* this WP extracts and polishes it. The pieces wired in here:
|
|
32
|
+
*
|
|
33
|
+
* - AUTOCOMMIT: when the variable is 'off' (default 'on') and the current
|
|
34
|
+
* transaction is idle, prepend `BEGIN` once before the next non-exempt
|
|
35
|
+
* statement. The set of exempt commands mirrors upstream
|
|
36
|
+
* `command_no_begin()` — transaction-control verbs and a handful of
|
|
37
|
+
* non-transactional DDL.
|
|
38
|
+
*
|
|
39
|
+
* - ON_ERROR_ROLLBACK: 'off' (default) | 'on' | 'interactive'. When active
|
|
40
|
+
* and we're inside a transaction, issue
|
|
41
|
+
* `SAVEPOINT pg_psql_temporary_savepoint` before the statement; on error
|
|
42
|
+
* `ROLLBACK TO`, on success `RELEASE` (unless the statement is itself a
|
|
43
|
+
* savepoint-management verb that already drops/replaces it).
|
|
44
|
+
*
|
|
45
|
+
* - FETCH_COUNT: integer; when >0 and the statement is a single SELECT-ish
|
|
46
|
+
* query, wrap it in `DECLARE _psql_cursor CURSOR FOR ...; FETCH FORWARD
|
|
47
|
+
* N FROM _psql_cursor` until exhausted. Non-SELECT statements fall back
|
|
48
|
+
* to the simple path.
|
|
49
|
+
*
|
|
50
|
+
* - SINGLESTEP: when `settings.singlestep` is true, print the SQL to stderr
|
|
51
|
+
* with the upstream confirmation banner and read one line from stdin.
|
|
52
|
+
* Input that starts with 'x' cancels the statement.
|
|
53
|
+
*
|
|
54
|
+
* - Timing: when `settings.timing` is true, measure wallclock around the
|
|
55
|
+
* send/print path and write a `Time: X.XXX ms` line to stdout (mirrors
|
|
56
|
+
* upstream's `printf` in common.c, and matches the existing mainloop
|
|
57
|
+
* test expectations).
|
|
58
|
+
*
|
|
59
|
+
* What's deliberately not done here:
|
|
60
|
+
*
|
|
61
|
+
* - SINGLELINE (-S): treating LF as a semicolon is a scanner concern, so it
|
|
62
|
+
* lives there — `scanSql` honours `ScanOptions.singleline` and the mainloop
|
|
63
|
+
* passes `settings.singleline` through on each pass. `sendQuery` itself
|
|
64
|
+
* needs no SINGLELINE branch; by the time a statement reaches here the
|
|
65
|
+
* scanner has already drawn the boundary.
|
|
66
|
+
*
|
|
67
|
+
* - Pipeline mode (-X / `\startpipeline`): upstream gates SendQuery on
|
|
68
|
+
* `pset.send_mode == PSQL_SEND_PIPELINE`; that path is owned by WP-21.
|
|
69
|
+
*
|
|
70
|
+
* - COPY FROM STDIN / TO STDOUT: upstream's ProcessResult dispatches on
|
|
71
|
+
* `PGRES_COPY_IN/OUT`. That belongs to WP-16; here we surface a clear
|
|
72
|
+
* error message if we ever see a copy result come back through
|
|
73
|
+
* `execSimple`.
|
|
74
|
+
*/
|
|
75
|
+
const readTxStatus = (conn) => {
|
|
76
|
+
return conn.txStatus ?? "I";
|
|
77
|
+
};
|
|
78
|
+
const SAVEPOINT_NAME = "pg_psql_temporary_savepoint";
|
|
79
|
+
const CURSOR_NAME = "_psql_cursor";
|
|
80
|
+
/**
|
|
81
|
+
* Strip leading whitespace and `--` line / slash-star block comments from
|
|
82
|
+
* `sql`. Mirrors what upstream psql's scanner advances past before handing a
|
|
83
|
+
* statement to `PQexec` — the server-reported error `position` is a 1-based
|
|
84
|
+
* offset into THAT trimmed buffer, so the `LINE N:` re-print computed from
|
|
85
|
+
* `count('\n')` in `sql.slice(0, position - 1)` aligns with vanilla output
|
|
86
|
+
* only when the same leading prelude is stripped here too.
|
|
87
|
+
*
|
|
88
|
+
* Block comments support nested depths (PG extension). Embedded comments
|
|
89
|
+
* mid-statement are intentionally NOT stripped — they participate in the
|
|
90
|
+
* line count of the executing statement, same as upstream.
|
|
91
|
+
*
|
|
92
|
+
* Exported for cmd_io / cmd_pipeline so backslash commands that capture or
|
|
93
|
+
* inspect `queryBuf` see the same shape that the wire and `lastQuery`
|
|
94
|
+
* receive.
|
|
95
|
+
*/
|
|
96
|
+
const stripLeadingCommentsAndWS = (sql) => {
|
|
97
|
+
let i = 0;
|
|
98
|
+
const n = sql.length;
|
|
99
|
+
while (i < n) {
|
|
100
|
+
const c = sql.charCodeAt(i);
|
|
101
|
+
if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11) {
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (c === 45 && sql.charCodeAt(i + 1) === 45) {
|
|
106
|
+
i += 2;
|
|
107
|
+
while (i < n && sql.charCodeAt(i) !== 10) i++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (c === 47 && sql.charCodeAt(i + 1) === 42) {
|
|
111
|
+
i += 2;
|
|
112
|
+
let depth = 1;
|
|
113
|
+
while (i < n && depth > 0) if (sql.charCodeAt(i) === 47 && sql.charCodeAt(i + 1) === 42) {
|
|
114
|
+
depth++;
|
|
115
|
+
i += 2;
|
|
116
|
+
} else if (sql.charCodeAt(i) === 42 && sql.charCodeAt(i + 1) === 47) {
|
|
117
|
+
depth--;
|
|
118
|
+
i += 2;
|
|
119
|
+
} else i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return i === 0 ? sql : sql.slice(i);
|
|
125
|
+
};
|
|
126
|
+
/** Strip leading whitespace and SQL comments, then upper-case for matching. */
|
|
127
|
+
const peekKeywords = (sql, count = 3) => {
|
|
128
|
+
let i = 0;
|
|
129
|
+
while (i < sql.length) {
|
|
130
|
+
const ch = sql[i];
|
|
131
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === "-" && sql[i + 1] === "-") {
|
|
136
|
+
const nl = sql.indexOf("\n", i + 2);
|
|
137
|
+
if (nl === -1) return [];
|
|
138
|
+
i = nl + 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (ch === "/" && sql[i + 1] === "*") {
|
|
142
|
+
const end = sql.indexOf("*/", i + 2);
|
|
143
|
+
if (end === -1) return [];
|
|
144
|
+
i = end + 2;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
return sql.slice(i).split(/[\s,;()]+/u, count + 1).slice(0, count).map((w) => w.toUpperCase());
|
|
150
|
+
};
|
|
151
|
+
/** Mirror of `command_no_begin()` in psql/common.c. */
|
|
152
|
+
const commandNoBegin = (sql) => {
|
|
153
|
+
const [w0, w1, w2, w3] = peekKeywords(sql, 4);
|
|
154
|
+
if (!w0) return false;
|
|
155
|
+
switch (w0) {
|
|
156
|
+
case "ABORT":
|
|
157
|
+
case "BEGIN":
|
|
158
|
+
case "COMMIT":
|
|
159
|
+
case "END":
|
|
160
|
+
case "ROLLBACK":
|
|
161
|
+
case "START":
|
|
162
|
+
case "SAVEPOINT":
|
|
163
|
+
case "RELEASE": return true;
|
|
164
|
+
case "PREPARE": return w1 === "TRANSACTION";
|
|
165
|
+
case "VACUUM": return true;
|
|
166
|
+
case "CLUSTER": return w1 === void 0 || w1 === "";
|
|
167
|
+
case "CREATE":
|
|
168
|
+
if (w1 === "DATABASE" || w1 === "TABLESPACE") return true;
|
|
169
|
+
if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
|
|
170
|
+
if (w1 === "UNIQUE" && w2 === "INDEX" && w3 === "CONCURRENTLY") return true;
|
|
171
|
+
return false;
|
|
172
|
+
case "DROP":
|
|
173
|
+
if (w1 === "DATABASE" || w1 === "TABLESPACE") return true;
|
|
174
|
+
if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
|
|
175
|
+
return false;
|
|
176
|
+
case "REINDEX":
|
|
177
|
+
if (w1 === "DATABASE" || w1 === "SYSTEM") return true;
|
|
178
|
+
if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
|
|
179
|
+
if (w1 === "TABLE" && w2 === "CONCURRENTLY") return true;
|
|
180
|
+
return false;
|
|
181
|
+
case "ALTER": return w1 === "SYSTEM";
|
|
182
|
+
case "DISCARD": return w1 === "ALL";
|
|
183
|
+
default: return false;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
/** True when the statement opens with SELECT / VALUES / TABLE / WITH. */
|
|
187
|
+
const isSelectCommand = (sql) => {
|
|
188
|
+
const [w0] = peekKeywords(sql, 1);
|
|
189
|
+
return w0 === "SELECT" || w0 === "VALUES" || w0 === "TABLE" || w0 === "WITH";
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Does this statement effectively destroy / replace the temporary savepoint?
|
|
193
|
+
* Upstream's `svpt_gone` flag is set when the user's command is one of
|
|
194
|
+
* COMMIT / ROLLBACK / SAVEPOINT / RELEASE. In those cases we must skip the
|
|
195
|
+
* matching RELEASE because the named savepoint no longer exists.
|
|
196
|
+
*/
|
|
197
|
+
const destroysSavepoint = (sql) => {
|
|
198
|
+
const [w0] = peekKeywords(sql, 1);
|
|
199
|
+
return w0 === "COMMIT" || w0 === "ROLLBACK" || w0 === "SAVEPOINT" || w0 === "RELEASE";
|
|
200
|
+
};
|
|
201
|
+
const pickPrinter = (settings) => {
|
|
202
|
+
switch (settings.popt.topt.format) {
|
|
203
|
+
case "aligned":
|
|
204
|
+
case "wrapped": return alignedPrinter;
|
|
205
|
+
case "unaligned": return unalignedPrinter;
|
|
206
|
+
case "csv": return csvPrinter;
|
|
207
|
+
case "json": return jsonPrinter;
|
|
208
|
+
case "html": return htmlPrinter;
|
|
209
|
+
case "asciidoc": return asciidocPrinter;
|
|
210
|
+
case "latex": return latexPrinter;
|
|
211
|
+
case "latex-longtable": return latexLongtablePrinter;
|
|
212
|
+
case "troff-ms": return troffMsPrinter;
|
|
213
|
+
default: return alignedPrinter;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Pick the output target for a query result.
|
|
218
|
+
*
|
|
219
|
+
* Precedence: explicit `oneShot` argument > the settings stash from
|
|
220
|
+
* `\o FILE` (WP-15) > the REPL context's `stdout`.
|
|
221
|
+
*/
|
|
222
|
+
const pickOut$1 = (ctx, oneShot) => {
|
|
223
|
+
if (oneShot) return oneShot;
|
|
224
|
+
return getQueryFout(ctx.settings) ?? ctx.stdout;
|
|
225
|
+
};
|
|
226
|
+
const readAutocommit = (settings) => settings.vars.asBool("AUTOCOMMIT", true);
|
|
227
|
+
const readOnErrorRollback = (settings) => {
|
|
228
|
+
const raw = settings.vars.get("ON_ERROR_ROLLBACK");
|
|
229
|
+
if (raw === void 0) return settings.onErrorRollback;
|
|
230
|
+
const v = raw.toLowerCase();
|
|
231
|
+
if (v === "interactive") return "interactive";
|
|
232
|
+
if (v === "on" || v === "true" || v === "yes" || v === "1") return "on";
|
|
233
|
+
return "off";
|
|
234
|
+
};
|
|
235
|
+
const readFetchCount = (settings) => {
|
|
236
|
+
const v = settings.vars.asInt("FETCH_COUNT", settings.fetchCount);
|
|
237
|
+
if (typeof v !== "number") return 0;
|
|
238
|
+
return Math.max(0, v | 0);
|
|
239
|
+
};
|
|
240
|
+
const readSinglestep = (settings) => settings.singlestep || settings.vars.asBool("SINGLESTEP", false);
|
|
241
|
+
/**
|
|
242
|
+
* SHOW_ALL_RESULTS controls multi-statement `\;` printing. Default 'on' —
|
|
243
|
+
* every result set is rendered. When 'off' / '0', only the LAST result set
|
|
244
|
+
* is printed (upstream's `pset.show_all_results` flag, consulted by
|
|
245
|
+
* `PrintQueryResults` in common.c).
|
|
246
|
+
*/
|
|
247
|
+
const readShowAllResults = (settings) => settings.vars.asBool("SHOW_ALL_RESULTS", true);
|
|
248
|
+
const writeError = (ctx, message) => {
|
|
249
|
+
ctx.stderr.write(`psql: ERROR: ${message}\n`);
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Render the verbosity-aware error report for the most recently captured
|
|
253
|
+
* query error and write it to `ctx.stderr`. The leading severity line gets
|
|
254
|
+
* the same `psql:[<file>:<n>]:` diagnostic prefix that upstream's
|
|
255
|
+
* `pg_log_pre_callback` prepends — matching the format the regression-
|
|
256
|
+
* derived conformance suite expects (e.g. `psql:<stdin>:N: ERROR: ...`).
|
|
257
|
+
* Subsequent layers (`LINE N: ...`, caret, `DETAIL: ...`, ...) follow on
|
|
258
|
+
* their own lines per {@link formatErrorReport}, unprefixed, to match
|
|
259
|
+
* libpq's `PQresultErrorMessage` output shape. When the captured error is
|
|
260
|
+
* missing (defensive — callers should always pair this with a preceding
|
|
261
|
+
* `recordError`) we fall back to the plain client-side {@link writeError}
|
|
262
|
+
* form so we never swallow the message entirely.
|
|
263
|
+
*
|
|
264
|
+
* Exported so the bind-path in mainloop can share the renderer.
|
|
265
|
+
*/
|
|
266
|
+
const writeQueryError = (ctx, fallbackMessage) => {
|
|
267
|
+
const e = ctx.settings.lastErrorResult;
|
|
268
|
+
if (!e || !e.message && !e.code && !e.sqlstate) {
|
|
269
|
+
writeError(ctx, fallbackMessage);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
|
|
273
|
+
const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
|
|
274
|
+
ctx.stderr.write(prefixed.join("\n") + "\n");
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Strip leading whitespace from a query and rebase a 1-based server position
|
|
278
|
+
* to match. Mirrors upstream psql/mainloop.c's behaviour: a line containing
|
|
279
|
+
* only a backslash command does not leave a `\n` in the query buffer, so the
|
|
280
|
+
* subsequent SQL statement starts at "line 1" of its own context rather than
|
|
281
|
+
* inheriting a blank line. Our mainloop doesn't perform that strip, so the
|
|
282
|
+
* captured `sqlText` sometimes has a leading `\n` (e.g. after `\set
|
|
283
|
+
* FETCH_COUNT 1\nSELECT error;`). Without this normalisation, `\errverbose`
|
|
284
|
+
* would render `LINE 2: SELECT error;` where upstream renders `LINE 1: …`.
|
|
285
|
+
*
|
|
286
|
+
* Returns the trimmed text and the rebased position. If the rebased
|
|
287
|
+
* position would land outside the trimmed text, it is dropped so the
|
|
288
|
+
* formatter skips the `LINE`/caret block instead of mis-pointing.
|
|
289
|
+
*/
|
|
290
|
+
const normaliseSqlAndPosition = (sqlText, position) => {
|
|
291
|
+
let leading = 0;
|
|
292
|
+
while (leading < sqlText.length) {
|
|
293
|
+
const ch = sqlText.charCodeAt(leading);
|
|
294
|
+
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13 && ch !== 12) break;
|
|
295
|
+
leading++;
|
|
296
|
+
}
|
|
297
|
+
if (leading === 0) return {
|
|
298
|
+
sqlText,
|
|
299
|
+
position
|
|
300
|
+
};
|
|
301
|
+
const trimmed = sqlText.slice(leading);
|
|
302
|
+
if (typeof position !== "string") return {
|
|
303
|
+
sqlText: trimmed,
|
|
304
|
+
position
|
|
305
|
+
};
|
|
306
|
+
const original = parseInt(position, 10);
|
|
307
|
+
if (!Number.isFinite(original) || original <= 0) return {
|
|
308
|
+
sqlText: trimmed,
|
|
309
|
+
position
|
|
310
|
+
};
|
|
311
|
+
const rebased = original - leading;
|
|
312
|
+
if (rebased <= 0 || rebased > trimmed.length) return {
|
|
313
|
+
sqlText: trimmed,
|
|
314
|
+
position: void 0
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
sqlText: trimmed,
|
|
318
|
+
position: String(rebased)
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Capture the full ErrorResponse-shaped payload from a thrown error.
|
|
323
|
+
*
|
|
324
|
+
* Our wire layer copies every named field of the server's ErrorResponse
|
|
325
|
+
* (severity / code / detail / hint / position / file / line / routine /
|
|
326
|
+
* …) onto the thrown Error as own properties (see `asThrowable` in
|
|
327
|
+
* `wire/connection.ts`). We mirror those onto `settings.lastErrorResult`
|
|
328
|
+
* so `\errverbose` can re-render the error in VERBOSE form — including
|
|
329
|
+
* the `LINE N: …` re-print + `^` pointer and the `LOCATION:` footer.
|
|
330
|
+
*
|
|
331
|
+
* `sqlText` is the originating SQL text from the caller; required so the
|
|
332
|
+
* `^` pointer can be positioned under the failing character. Leading
|
|
333
|
+
* whitespace is stripped (and `position` is rebased) so the `LINE N`
|
|
334
|
+
* counter reflects offsets within the user's statement rather than any
|
|
335
|
+
* buffer noise carried over from prior backslash commands.
|
|
336
|
+
*/
|
|
337
|
+
const captureLastError = (settings, err, sqlText) => {
|
|
338
|
+
const fallbackMessage = err instanceof Error ? err.message : String(err);
|
|
339
|
+
const e = err ?? {};
|
|
340
|
+
const code = e.code;
|
|
341
|
+
const normalised = normaliseSqlAndPosition(sqlText, e.position);
|
|
342
|
+
settings.lastErrorResult = {
|
|
343
|
+
severity: e.severity,
|
|
344
|
+
code,
|
|
345
|
+
sqlstate: code,
|
|
346
|
+
message: e.message ?? fallbackMessage,
|
|
347
|
+
detail: e.detail,
|
|
348
|
+
hint: e.hint,
|
|
349
|
+
position: normalised.position,
|
|
350
|
+
internalPosition: e.internalPosition,
|
|
351
|
+
internalQuery: e.internalQuery,
|
|
352
|
+
where: e.where,
|
|
353
|
+
schema: e.schema,
|
|
354
|
+
table: e.table,
|
|
355
|
+
column: e.column,
|
|
356
|
+
dataType: e.dataType,
|
|
357
|
+
constraint: e.constraint,
|
|
358
|
+
file: e.file,
|
|
359
|
+
line: e.line,
|
|
360
|
+
routine: e.routine,
|
|
361
|
+
sqlText: normalised.sqlText
|
|
362
|
+
};
|
|
363
|
+
return settings.lastErrorResult.message ?? fallbackMessage;
|
|
364
|
+
};
|
|
365
|
+
const recordError = (ctx, err, sqlText = "") => captureLastError(ctx.settings, err, sqlText);
|
|
366
|
+
/**
|
|
367
|
+
* Update the per-statement diagnostic psql variables that upstream's
|
|
368
|
+
* `SetResultVariables` / `SetErrorVariables` in `src/bin/psql/common.c`
|
|
369
|
+
* maintains. Called after every dispatched statement (success and error
|
|
370
|
+
* paths) so `\echo :LAST_ERROR_MESSAGE` and friends produce the same
|
|
371
|
+
* values vanilla psql does.
|
|
372
|
+
*
|
|
373
|
+
* - `SQLSTATE` SQLSTATE of the *most recent* statement —
|
|
374
|
+
* `"00000"` on success, the server-reported code
|
|
375
|
+
* on error (defaults to `"XX000"` when missing).
|
|
376
|
+
* - `ERROR` `"true"` if the most recent statement failed,
|
|
377
|
+
* else `"false"`.
|
|
378
|
+
* - `ROW_COUNT` affected/returned row count of the most recent
|
|
379
|
+
* statement (from libpq's `PQcmdTuples`). `"0"`
|
|
380
|
+
* on error.
|
|
381
|
+
* - `LAST_ERROR_*` sticky — only mutated on error. Mirrors
|
|
382
|
+
* upstream's "preserve until the next failure"
|
|
383
|
+
* contract so a successful statement does not
|
|
384
|
+
* clobber the prior error info.
|
|
385
|
+
*
|
|
386
|
+
* Exported so mainloop's bind / pipeline paths (which bypass {@link
|
|
387
|
+
* sendQuery}) can share the same updater.
|
|
388
|
+
*/
|
|
389
|
+
const refreshErrorVars = (settings, outcome) => {
|
|
390
|
+
const { vars } = settings;
|
|
391
|
+
if (outcome.kind === "error") {
|
|
392
|
+
const last = settings.lastErrorResult;
|
|
393
|
+
const code = last?.code ?? last?.sqlstate ?? "XX000";
|
|
394
|
+
const message = last?.message ?? "";
|
|
395
|
+
vars.set("LAST_ERROR_MESSAGE", message);
|
|
396
|
+
vars.set("LAST_ERROR_SQLSTATE", code);
|
|
397
|
+
vars.set("SQLSTATE", code);
|
|
398
|
+
vars.set("ERROR", "true");
|
|
399
|
+
vars.set("ROW_COUNT", "0");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
vars.set("SQLSTATE", "00000");
|
|
403
|
+
vars.set("ERROR", "false");
|
|
404
|
+
const rc = outcome.rowCount ?? 0;
|
|
405
|
+
vars.set("ROW_COUNT", String(rc));
|
|
406
|
+
};
|
|
407
|
+
const readOneLine = (stdin) => new Promise((resolve) => {
|
|
408
|
+
let buf = "";
|
|
409
|
+
let resolved = false;
|
|
410
|
+
const onData = (chunk) => {
|
|
411
|
+
buf += chunk.toString();
|
|
412
|
+
const nl = buf.indexOf("\n");
|
|
413
|
+
if (nl !== -1) {
|
|
414
|
+
const line = buf.slice(0, nl);
|
|
415
|
+
cleanup();
|
|
416
|
+
if (!resolved) {
|
|
417
|
+
resolved = true;
|
|
418
|
+
resolve(line);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
const onEnd = () => {
|
|
423
|
+
cleanup();
|
|
424
|
+
if (!resolved) {
|
|
425
|
+
resolved = true;
|
|
426
|
+
resolve(buf);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const cleanup = () => {
|
|
430
|
+
stdin.off("data", onData);
|
|
431
|
+
stdin.off("end", onEnd);
|
|
432
|
+
stdin.off("close", onEnd);
|
|
433
|
+
};
|
|
434
|
+
stdin.on("data", onData);
|
|
435
|
+
stdin.once("end", onEnd);
|
|
436
|
+
stdin.once("close", onEnd);
|
|
437
|
+
});
|
|
438
|
+
const confirmSinglestep = async (ctx, sql) => {
|
|
439
|
+
ctx.stderr.write(`***(Single step mode: verify command)*******************************************\n${sql}\n***(press return to proceed or enter x and return to cancel)********************\n`);
|
|
440
|
+
return !(await readOneLine(ctx.stdin)).trim().toLowerCase().startsWith("x");
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* Reconstruct the libpq-style CommandComplete tag from the parsed parts our
|
|
444
|
+
* wire layer stores on a ResultSet. INSERT carries `oid` + `rowCount`; the
|
|
445
|
+
* other DML verbs (UPDATE/DELETE/MERGE/SELECT/MOVE/FETCH/COPY) carry just a
|
|
446
|
+
* `rowCount`; DDL has neither.
|
|
447
|
+
*
|
|
448
|
+
* Matches upstream psql's `PQcmdStatus(conn)` output (which is the raw tag
|
|
449
|
+
* the server sent — we round-trip it through our parser).
|
|
450
|
+
*/
|
|
451
|
+
const formatCommandTag = (rs) => {
|
|
452
|
+
const command = (rs.command || "").trim();
|
|
453
|
+
if (command.length === 0) return "";
|
|
454
|
+
if (command === "INSERT") return `INSERT ${rs.oid ?? 0} ${rs.rowCount ?? 0}`;
|
|
455
|
+
if (rs.rowCount !== null && rs.rowCount !== void 0) return `${command} ${rs.rowCount}`;
|
|
456
|
+
return command;
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* Decide whether the pager should activate for the given batch of result
|
|
460
|
+
* sets. We page when ANY of the sets-that-will-be-printed (i.e. tuples-
|
|
461
|
+
* producing, and not gated out by SHOW_ALL_RESULTS) crosses the threshold.
|
|
462
|
+
* The decision is centralised here so callers can override individual
|
|
463
|
+
* decision inputs (e.g. tests that inject a fake `output`).
|
|
464
|
+
*/
|
|
465
|
+
const pickPagerDecision = (ctx, results, out) => {
|
|
466
|
+
const popt = ctx.settings.popt.topt;
|
|
467
|
+
if (popt.pager === "off") return false;
|
|
468
|
+
const redirectedOutput = getQueryFout(ctx.settings) !== null;
|
|
469
|
+
if (redirectedOutput) return false;
|
|
470
|
+
const showAll = readShowAllResults(ctx.settings);
|
|
471
|
+
const lastIdx = results.length - 1;
|
|
472
|
+
for (let i = 0; i < results.length; i++) {
|
|
473
|
+
const rs = results[i];
|
|
474
|
+
if (rs.fields.length === 0) continue;
|
|
475
|
+
if (!(showAll || i === lastIdx)) continue;
|
|
476
|
+
if (shouldPage({
|
|
477
|
+
pager: popt.pager,
|
|
478
|
+
pagerMinLines: popt.pagerMinLines,
|
|
479
|
+
rowCount: rs.rows.length,
|
|
480
|
+
colCount: rs.fields.length,
|
|
481
|
+
output: out,
|
|
482
|
+
redirectedOutput
|
|
483
|
+
})) return true;
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
};
|
|
487
|
+
const renderResultSets = async (ctx, results, out) => {
|
|
488
|
+
const printer = pickPrinter(ctx.settings);
|
|
489
|
+
let rowsAffected = 0;
|
|
490
|
+
let rowsPrinted = 0;
|
|
491
|
+
const showAll = readShowAllResults(ctx.settings);
|
|
492
|
+
const lastIdx = results.length - 1;
|
|
493
|
+
const tuplesOnly = ctx.settings.popt.topt.tuplesOnly;
|
|
494
|
+
const pager = pickPagerDecision(ctx, results, out) ? openPager({
|
|
495
|
+
pager: ctx.settings.popt.topt.pager,
|
|
496
|
+
pagerMinLines: ctx.settings.popt.topt.pagerMinLines,
|
|
497
|
+
stdout: out
|
|
498
|
+
}) : null;
|
|
499
|
+
const sink = pager?.spawned ? pager.out : out;
|
|
500
|
+
try {
|
|
501
|
+
for (let i = 0; i < results.length; i++) {
|
|
502
|
+
const rs = results[i];
|
|
503
|
+
const shouldEmit = showAll || i === lastIdx;
|
|
504
|
+
if (rs.copyOutBytes && rs.copyOutBytes.length > 0) for (const chunk of rs.copyOutBytes) sink.write(chunk);
|
|
505
|
+
if (rs.fields.length === 0) {
|
|
506
|
+
if (shouldEmit && !tuplesOnly && !ctx.settings.quiet && !rs.copyOutBytes) {
|
|
507
|
+
const tag = formatCommandTag(rs);
|
|
508
|
+
if (tag.length > 0) sink.write(`${tag}\n`);
|
|
509
|
+
}
|
|
510
|
+
rowsAffected += rs.rowCount ?? 0;
|
|
511
|
+
} else {
|
|
512
|
+
if (shouldEmit) await printer.printQuery(rs, ctx.settings.popt, sink);
|
|
513
|
+
rowsPrinted += rs.rows.length;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} finally {
|
|
517
|
+
if (pager?.spawned) try {
|
|
518
|
+
await pager.close();
|
|
519
|
+
} catch {}
|
|
520
|
+
}
|
|
521
|
+
const lastRowCount = results.length === 0 ? null : (() => {
|
|
522
|
+
const rs = results[results.length - 1];
|
|
523
|
+
if (rs.fields.length > 0) return rs.rows.length;
|
|
524
|
+
return rs.rowCount ?? null;
|
|
525
|
+
})();
|
|
526
|
+
return {
|
|
527
|
+
rowsAffected,
|
|
528
|
+
rowsPrinted,
|
|
529
|
+
lastRowCount
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
* Render a single {@link ResultSet} through the active printer and the
|
|
534
|
+
* configured output target (respecting `\o FILE` redirects). Used by the
|
|
535
|
+
* `\bind` / extended-query path in {@link mainloop.dispatchSendQuery} which
|
|
536
|
+
* comes back with a single result instead of the array shape `execSimple`
|
|
537
|
+
* produces. Returns a tally consistent with {@link renderResultSets}.
|
|
538
|
+
*/
|
|
539
|
+
const renderResultSet = (ctx, rs, out) => {
|
|
540
|
+
return renderResultSets(ctx, [rs], out ?? pickOut$1(ctx));
|
|
541
|
+
};
|
|
542
|
+
/**
|
|
543
|
+
* Re-base a server-side `position` field so it points into the user's
|
|
544
|
+
* original SQL rather than the synthetic statement we actually sent.
|
|
545
|
+
*
|
|
546
|
+
* The FETCH_COUNT path sends `DECLARE _psql_cursor NO SCROLL CURSOR FOR
|
|
547
|
+
* <userSql>` for the DECLARE leg and `FETCH FORWARD N FROM _psql_cursor`
|
|
548
|
+
* for each fetch. Server error positions (`P` field) come back in the
|
|
549
|
+
* coordinates of whatever query we sent:
|
|
550
|
+
*
|
|
551
|
+
* - DECLARE-time parser/planner errors carry a position into the DECLARE
|
|
552
|
+
* statement. We subtract the length of the prefix (`DECLARE … FOR `)
|
|
553
|
+
* so the caret lands under the failing token in `userSql`.
|
|
554
|
+
*
|
|
555
|
+
* - FETCH-time runtime errors come from executing the cursor's underlying
|
|
556
|
+
* query (which IS `userSql`). The server reports the position relative
|
|
557
|
+
* to that underlying query, so it's already in `userSql` coordinates
|
|
558
|
+
* and we leave it alone.
|
|
559
|
+
*
|
|
560
|
+
* If we can't rebase a DECLARE-coord position into `userSql` bounds, we
|
|
561
|
+
* strip it rather than render a caret pointing past end-of-line.
|
|
562
|
+
*/
|
|
563
|
+
const rebasePositionForCursor = (err, wrapper, userSql) => {
|
|
564
|
+
if (!err || typeof err !== "object") return;
|
|
565
|
+
const e = err;
|
|
566
|
+
if (typeof e.position !== "string") return;
|
|
567
|
+
const original = parseInt(e.position, 10);
|
|
568
|
+
if (!Number.isFinite(original) || original <= 0) return;
|
|
569
|
+
const stripped = userSql.replace(/;\s*$/u, "");
|
|
570
|
+
const offset = wrapper.indexOf(stripped);
|
|
571
|
+
if (offset === -1) return;
|
|
572
|
+
const rebased = original - offset;
|
|
573
|
+
if (rebased <= 0 || rebased > userSql.length) {
|
|
574
|
+
delete e.position;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
e.position = String(rebased);
|
|
578
|
+
};
|
|
579
|
+
const runCursorLoop = async (ctx, sql, fetchCount, out) => {
|
|
580
|
+
if (!ctx.settings.db) throw new Error("no connection to the server");
|
|
581
|
+
const db = ctx.settings.db;
|
|
582
|
+
const initiallyIdle = readTxStatus(db) === "I";
|
|
583
|
+
if (initiallyIdle) await db.execSimple("BEGIN");
|
|
584
|
+
const stripped = sql.replace(/;\s*$/u, "");
|
|
585
|
+
const declared = `DECLARE ${CURSOR_NAME} NO SCROLL CURSOR FOR ${stripped}`;
|
|
586
|
+
const fetchSql = `FETCH FORWARD ${String(fetchCount)} FROM ${CURSOR_NAME}`;
|
|
587
|
+
const rowsAffected = 0;
|
|
588
|
+
let rowsPrinted = 0;
|
|
589
|
+
let cursorOpen = false;
|
|
590
|
+
let currentWrapper = declared;
|
|
591
|
+
const printer = pickPrinter(ctx.settings);
|
|
592
|
+
let merged = null;
|
|
593
|
+
try {
|
|
594
|
+
await db.execSimple(declared);
|
|
595
|
+
cursorOpen = true;
|
|
596
|
+
while (true) {
|
|
597
|
+
currentWrapper = fetchSql;
|
|
598
|
+
const sets = await db.execSimple(fetchSql);
|
|
599
|
+
if (sets.length === 0) break;
|
|
600
|
+
const rs = sets[sets.length - 1];
|
|
601
|
+
const chunkRows = rs.rows.length;
|
|
602
|
+
if (chunkRows === 0) break;
|
|
603
|
+
if (merged === null) merged = {
|
|
604
|
+
command: rs.command,
|
|
605
|
+
fields: rs.fields,
|
|
606
|
+
rows: rs.rows.slice(),
|
|
607
|
+
rowCount: rs.rowCount,
|
|
608
|
+
oid: rs.oid,
|
|
609
|
+
notices: rs.notices
|
|
610
|
+
};
|
|
611
|
+
else for (const row of rs.rows) merged.rows.push(row);
|
|
612
|
+
rowsPrinted += chunkRows;
|
|
613
|
+
if (chunkRows < fetchCount) break;
|
|
614
|
+
}
|
|
615
|
+
if (merged !== null) {
|
|
616
|
+
merged.rowCount = merged.rows.length;
|
|
617
|
+
await printer.printQuery(merged, ctx.settings.popt, out);
|
|
618
|
+
}
|
|
619
|
+
await db.execSimple(`CLOSE ${CURSOR_NAME}`);
|
|
620
|
+
cursorOpen = false;
|
|
621
|
+
if (initiallyIdle) await db.execSimple("COMMIT");
|
|
622
|
+
return {
|
|
623
|
+
rowsAffected,
|
|
624
|
+
rowsPrinted,
|
|
625
|
+
lastRowCount: rowsPrinted
|
|
626
|
+
};
|
|
627
|
+
} catch (err) {
|
|
628
|
+
if (merged !== null) {
|
|
629
|
+
merged.rowCount = merged.rows.length;
|
|
630
|
+
const partialOpts = {
|
|
631
|
+
...ctx.settings.popt,
|
|
632
|
+
topt: {
|
|
633
|
+
...ctx.settings.popt.topt,
|
|
634
|
+
defaultFooter: false,
|
|
635
|
+
stopTable: false
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
try {
|
|
639
|
+
await printer.printQuery(merged, partialOpts, out);
|
|
640
|
+
} catch {}
|
|
641
|
+
}
|
|
642
|
+
rebasePositionForCursor(err, currentWrapper, sql);
|
|
643
|
+
if (cursorOpen) try {
|
|
644
|
+
await db.execSimple(`CLOSE ${CURSOR_NAME}`);
|
|
645
|
+
} catch {}
|
|
646
|
+
if (initiallyIdle) try {
|
|
647
|
+
await db.execSimple("ROLLBACK");
|
|
648
|
+
} catch {}
|
|
649
|
+
throw err;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const executeAndPrint = async (ctx, sqlRaw, opts = {}) => {
|
|
653
|
+
const sql = stripLeadingCommentsAndWS(sqlRaw);
|
|
654
|
+
const started = ctx.settings.timing ? performance.now() : 0;
|
|
655
|
+
const stats = {
|
|
656
|
+
rowsAffected: 0,
|
|
657
|
+
rowsPrinted: 0,
|
|
658
|
+
fetched: false,
|
|
659
|
+
hadError: false,
|
|
660
|
+
durationMs: 0
|
|
661
|
+
};
|
|
662
|
+
if (!ctx.settings.db) {
|
|
663
|
+
writeError(ctx, "no connection to the server");
|
|
664
|
+
stats.hadError = true;
|
|
665
|
+
return stats;
|
|
666
|
+
}
|
|
667
|
+
const out = pickOut$1(ctx, opts.oneShotOut);
|
|
668
|
+
const fetchCount = readFetchCount(ctx.settings);
|
|
669
|
+
let lastRowCount = null;
|
|
670
|
+
try {
|
|
671
|
+
if (fetchCount > 0 && isSelectCommand(sql)) {
|
|
672
|
+
const r = await runCursorLoop(ctx, sql, fetchCount, out);
|
|
673
|
+
stats.rowsAffected = r.rowsAffected;
|
|
674
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
675
|
+
stats.fetched = true;
|
|
676
|
+
lastRowCount = r.lastRowCount;
|
|
677
|
+
} else {
|
|
678
|
+
const results = await ctx.settings.db.execSimple(sql);
|
|
679
|
+
const r = await renderResultSets(ctx, results, out);
|
|
680
|
+
stats.rowsAffected = r.rowsAffected;
|
|
681
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
682
|
+
lastRowCount = r.lastRowCount;
|
|
683
|
+
}
|
|
684
|
+
} catch (err) {
|
|
685
|
+
const partial = err.partialResults;
|
|
686
|
+
if (partial && partial.length > 0) try {
|
|
687
|
+
const r = await renderResultSets(ctx, partial, out);
|
|
688
|
+
stats.rowsAffected = r.rowsAffected;
|
|
689
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
690
|
+
} catch {}
|
|
691
|
+
const message = recordError(ctx, err, sql);
|
|
692
|
+
writeQueryError(ctx, message);
|
|
693
|
+
stats.hadError = true;
|
|
694
|
+
} finally {
|
|
695
|
+
if (ctx.settings.timing) {
|
|
696
|
+
stats.durationMs = performance.now() - started;
|
|
697
|
+
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
refreshErrorVars(ctx.settings, stats.hadError ? { kind: "error" } : {
|
|
701
|
+
kind: "success",
|
|
702
|
+
rowCount: lastRowCount
|
|
703
|
+
});
|
|
704
|
+
return stats;
|
|
705
|
+
};
|
|
706
|
+
const sendQuery = async (ctx, sqlRaw, opts = {}) => {
|
|
707
|
+
const sql = stripLeadingCommentsAndWS(sqlRaw);
|
|
708
|
+
const stats = {
|
|
709
|
+
rowsAffected: 0,
|
|
710
|
+
rowsPrinted: 0,
|
|
711
|
+
fetched: false,
|
|
712
|
+
hadError: false,
|
|
713
|
+
durationMs: 0
|
|
714
|
+
};
|
|
715
|
+
ctx.settings.lastQuery = sql;
|
|
716
|
+
if (!ctx.settings.db) {
|
|
717
|
+
writeError(ctx, "no connection to the server");
|
|
718
|
+
stats.hadError = true;
|
|
719
|
+
return stats;
|
|
720
|
+
}
|
|
721
|
+
if (readSinglestep(ctx.settings)) {
|
|
722
|
+
if (!await confirmSinglestep(ctx, sql)) {
|
|
723
|
+
stats.hadError = true;
|
|
724
|
+
ctx.settings.lastErrorResult = { message: "command cancelled by user" };
|
|
725
|
+
return stats;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const db = ctx.settings.db;
|
|
729
|
+
const autocommit = readAutocommit(ctx.settings);
|
|
730
|
+
const onErrorRollback = readOnErrorRollback(ctx.settings);
|
|
731
|
+
const interactive = !ctx.settings.notty;
|
|
732
|
+
const started = ctx.settings.timing ? performance.now() : 0;
|
|
733
|
+
if (!autocommit && readTxStatus(db) === "I" && !commandNoBegin(sql)) try {
|
|
734
|
+
await db.execSimple("BEGIN");
|
|
735
|
+
} catch (err) {
|
|
736
|
+
const message = recordError(ctx, err);
|
|
737
|
+
writeQueryError(ctx, message);
|
|
738
|
+
stats.hadError = true;
|
|
739
|
+
if (ctx.settings.timing) {
|
|
740
|
+
stats.durationMs = performance.now() - started;
|
|
741
|
+
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
742
|
+
}
|
|
743
|
+
return stats;
|
|
744
|
+
}
|
|
745
|
+
const savepointActive = onErrorRollback !== "off" && (onErrorRollback === "on" || onErrorRollback === "interactive" && interactive) && readTxStatus(db) === "T";
|
|
746
|
+
let savepointIssued = false;
|
|
747
|
+
if (savepointActive) try {
|
|
748
|
+
await db.execSimple(`SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
749
|
+
savepointIssued = true;
|
|
750
|
+
} catch (err) {
|
|
751
|
+
const message = recordError(ctx, err);
|
|
752
|
+
writeQueryError(ctx, message);
|
|
753
|
+
stats.hadError = true;
|
|
754
|
+
if (ctx.settings.timing) {
|
|
755
|
+
stats.durationMs = performance.now() - started;
|
|
756
|
+
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
757
|
+
}
|
|
758
|
+
return stats;
|
|
759
|
+
}
|
|
760
|
+
const out = pickOut$1(ctx, opts.oneShotOut);
|
|
761
|
+
const fetchCount = readFetchCount(ctx.settings);
|
|
762
|
+
let lastRowCount = null;
|
|
763
|
+
try {
|
|
764
|
+
if (fetchCount > 0 && isSelectCommand(sql)) {
|
|
765
|
+
const r = await runCursorLoop(ctx, sql, fetchCount, out);
|
|
766
|
+
stats.rowsAffected = r.rowsAffected;
|
|
767
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
768
|
+
stats.fetched = true;
|
|
769
|
+
lastRowCount = r.lastRowCount;
|
|
770
|
+
} else {
|
|
771
|
+
const results = await db.execSimple(sql);
|
|
772
|
+
const r = await renderResultSets(ctx, results, out);
|
|
773
|
+
stats.rowsAffected = r.rowsAffected;
|
|
774
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
775
|
+
lastRowCount = r.lastRowCount;
|
|
776
|
+
}
|
|
777
|
+
} catch (err) {
|
|
778
|
+
const partial = err.partialResults;
|
|
779
|
+
if (partial && partial.length > 0) try {
|
|
780
|
+
const r = await renderResultSets(ctx, partial, out);
|
|
781
|
+
stats.rowsAffected = r.rowsAffected;
|
|
782
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
783
|
+
} catch {}
|
|
784
|
+
const message = recordError(ctx, err, sql);
|
|
785
|
+
writeQueryError(ctx, message);
|
|
786
|
+
stats.hadError = true;
|
|
787
|
+
}
|
|
788
|
+
if (savepointIssued) try {
|
|
789
|
+
if (stats.hadError) {
|
|
790
|
+
await db.execSimple(`ROLLBACK TO SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
791
|
+
await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
792
|
+
} else if (!destroysSavepoint(sql) && readTxStatus(db) === "T") await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
if (!stats.hadError) {
|
|
795
|
+
const message = recordError(ctx, err);
|
|
796
|
+
writeQueryError(ctx, message);
|
|
797
|
+
stats.hadError = true;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (stats.hadError && ctx.settings.echo === "errors") {
|
|
801
|
+
let stmt = sql;
|
|
802
|
+
while (true) {
|
|
803
|
+
const before = stmt.length;
|
|
804
|
+
stmt = stmt.replace(/^[ \t\n\r\f]+/, "");
|
|
805
|
+
stmt = stmt.replace(/^--[^\n\r]*/, "");
|
|
806
|
+
if (stmt.length === before) break;
|
|
807
|
+
}
|
|
808
|
+
if (stmt.endsWith("\n")) stmt = stmt.slice(0, -1);
|
|
809
|
+
ctx.stderr.write(`STATEMENT: ${stmt}\n`);
|
|
810
|
+
}
|
|
811
|
+
refreshErrorVars(ctx.settings, stats.hadError ? { kind: "error" } : {
|
|
812
|
+
kind: "success",
|
|
813
|
+
rowCount: lastRowCount
|
|
814
|
+
});
|
|
815
|
+
if (ctx.settings.timing) {
|
|
816
|
+
stats.durationMs = performance.now() - started;
|
|
817
|
+
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
818
|
+
}
|
|
819
|
+
return stats;
|
|
820
|
+
};
|
|
821
|
+
const psqlExec = async (conn, sql, ignoreError = false) => {
|
|
822
|
+
try {
|
|
823
|
+
const sets = await conn.execSimple(sql);
|
|
824
|
+
if (sets.length === 0) return null;
|
|
825
|
+
return sets[sets.length - 1];
|
|
826
|
+
} catch (err) {
|
|
827
|
+
if (ignoreError) return null;
|
|
828
|
+
throw err;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
const __testing = {
|
|
832
|
+
commandNoBegin,
|
|
833
|
+
isSelectCommand,
|
|
834
|
+
destroysSavepoint,
|
|
835
|
+
peekKeywords,
|
|
836
|
+
SAVEPOINT_NAME,
|
|
837
|
+
CURSOR_NAME
|
|
838
|
+
};
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region src/psql/command/cmd_io.ts
|
|
841
|
+
/**
|
|
842
|
+
* psql I/O & control backslash commands.
|
|
843
|
+
*
|
|
844
|
+
* TypeScript port of the following `exec_command_*` functions in upstream
|
|
845
|
+
* PostgreSQL's `src/bin/psql/command.c`:
|
|
846
|
+
*
|
|
847
|
+
* - `\i`, `\include` → exec_command_include (normal)
|
|
848
|
+
* - `\ir`, `\include_relative` → exec_command_include (relative=true)
|
|
849
|
+
* - `\o`, `\out` → exec_command_out
|
|
850
|
+
* - `\w`, `\write` → exec_command_write
|
|
851
|
+
* - `\g` → exec_command_g
|
|
852
|
+
* - `\gx` → exec_command_g (force_expanded=true)
|
|
853
|
+
* - `\gset` → exec_command_gset
|
|
854
|
+
* - `\gdesc` → exec_command_gdesc
|
|
855
|
+
* - `\gexec` → exec_command_gexec
|
|
856
|
+
* - `\watch` → exec_command_watch
|
|
857
|
+
*
|
|
858
|
+
* Each is exported as a `BackslashCmdSpec` and registered via
|
|
859
|
+
* {@link registerIoCommands}. The single line that wires us into the
|
|
860
|
+
* default dispatcher lives in `dispatch.ts::defaultRegistry()`.
|
|
861
|
+
*
|
|
862
|
+
* # Integration touch-points and known limitations
|
|
863
|
+
*
|
|
864
|
+
* Several of these commands really want to participate in the mainloop's
|
|
865
|
+
* scanner/printer pipeline. This WP keeps `src/psql/core/mainloop.ts`
|
|
866
|
+
* untouched, so we provide the data structures and let a follow-up WP wire
|
|
867
|
+
* the consumption sites. Limitations documented per-command:
|
|
868
|
+
*
|
|
869
|
+
* - `\i FILE` enqueues the file's contents on a small input queue
|
|
870
|
+
* (`./inputQueue.ts`) AND, as a stop-gap, executes the file's SQL
|
|
871
|
+
* directly via `Connection.execSimple`. Backslash commands embedded in
|
|
872
|
+
* the file are NOT processed by the scanner; the include is a "best
|
|
873
|
+
* effort: run as one big SQL blob". Once mainloop adopts the queue API
|
|
874
|
+
* this becomes a true include.
|
|
875
|
+
*
|
|
876
|
+
* - `\o FILE` opens a writable stream and stashes it under a symbol on
|
|
877
|
+
* `settings`. We expose a getter (`getQueryFout`) for the mainloop to
|
|
878
|
+
* consult; until that wiring happens, query output continues to flow
|
|
879
|
+
* to the mainloop's `ctx.stdout`. The stash + close-on-rebind logic is
|
|
880
|
+
* in place and fully tested.
|
|
881
|
+
*
|
|
882
|
+
* - `\g` (no arg) executes the current queryBuf directly through
|
|
883
|
+
* `Connection.execSimple` and renders via the aligned printer. This
|
|
884
|
+
* duplicates a tiny slice of mainloop's send/print pipeline, which is
|
|
885
|
+
* fine for the bytewise-simple cases this WP needs to support. For
|
|
886
|
+
* `\g FILE` / `\g |cmd` the output goes through the temporary writer.
|
|
887
|
+
*
|
|
888
|
+
* - `\gx` toggles `topt.expanded` for the single execution and restores
|
|
889
|
+
* the prior value in a `try { ... } finally { ... }`.
|
|
890
|
+
*
|
|
891
|
+
* - `\gset [PREFIX]` executes via `execSimple`, requires the last result
|
|
892
|
+
* to have exactly one row, and stores `${prefix}${colname}` → value
|
|
893
|
+
* for each column on `settings.vars`.
|
|
894
|
+
*
|
|
895
|
+
* - `\gdesc` parses the buffered query with the extended protocol
|
|
896
|
+
* (Parse + Describe by statement, no Execute), then assembles a
|
|
897
|
+
* synthetic `Column / Type` ResultSet and renders it through the
|
|
898
|
+
* active printer (`alignedPrinter` by default; the format picker
|
|
899
|
+
* honours `\pset format`). Tuples-only mode (`\t on`) and `\o FILE`
|
|
900
|
+
* redirects ride along automatically because the same ResultSet
|
|
901
|
+
* goes through the same printer the REPL would use for a query.
|
|
902
|
+
*
|
|
903
|
+
* - `\gexec` iterates the cells of the last result row-major and feeds
|
|
904
|
+
* each non-null cell back as SQL through `execSimple`. Each statement's
|
|
905
|
+
* output is rendered to stdout (or to the active queryFout stash).
|
|
906
|
+
*
|
|
907
|
+
* - `\watch [INTERVAL]` re-executes the queryBuf every `INTERVAL` seconds
|
|
908
|
+
* (default 2) until SIGINT or until the iteration count limit is hit.
|
|
909
|
+
* We hook SIGINT via a transient listener that's removed on completion.
|
|
910
|
+
* Tests bypass the listener by using an AbortController exposed via
|
|
911
|
+
* `WATCH_TEST_CONTROLLER`.
|
|
912
|
+
*
|
|
913
|
+
* # Error format
|
|
914
|
+
*
|
|
915
|
+
* Upstream prints `<cmd>: <msg>` to stderr and returns failure. We mirror
|
|
916
|
+
* that and also stash the message on `settings.lastErrorResult` so the
|
|
917
|
+
* mainloop's `writeError()` wrapper can pick it up.
|
|
918
|
+
*/
|
|
919
|
+
const QUERY_FOUT_KEY = Symbol.for("neonctl.psql.queryFout");
|
|
920
|
+
/**
|
|
921
|
+
* Return the currently active queryFout stream (or `null` if none).
|
|
922
|
+
* The mainloop is encouraged to call this in lieu of writing directly to
|
|
923
|
+
* `ctx.stdout` for query results.
|
|
924
|
+
*/
|
|
925
|
+
const getQueryFout = (settings) => {
|
|
926
|
+
return settings[QUERY_FOUT_KEY]?.stream ?? null;
|
|
927
|
+
};
|
|
928
|
+
const setQueryFout = (settings, entry) => {
|
|
929
|
+
const stash = settings;
|
|
930
|
+
if (entry === null) stash[QUERY_FOUT_KEY] = void 0;
|
|
931
|
+
else stash[QUERY_FOUT_KEY] = entry;
|
|
932
|
+
};
|
|
933
|
+
const closeQueryFout = async (settings) => {
|
|
934
|
+
const stash = settings;
|
|
935
|
+
const prev = stash[QUERY_FOUT_KEY];
|
|
936
|
+
if (prev) {
|
|
937
|
+
stash[QUERY_FOUT_KEY] = void 0;
|
|
938
|
+
await prev.close();
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const WATCH_TEST_CONTROLLER = { ref: null };
|
|
942
|
+
const errResult$1 = (ctx, message) => {
|
|
943
|
+
ctx.settings.lastErrorResult = { message };
|
|
944
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
945
|
+
writeErr(`${prefix}\\${ctx.cmdName}: ${message}\n`);
|
|
946
|
+
return {
|
|
947
|
+
status: "error",
|
|
948
|
+
errorWritten: true
|
|
949
|
+
};
|
|
950
|
+
};
|
|
951
|
+
/**
|
|
952
|
+
* Reject buffer-consuming commands when an extended pipeline is open. Upstream
|
|
953
|
+
* `exec_command_g` / `gx` / `gset` / `gexec` / `watch` all guard with
|
|
954
|
+
* `PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF` and emit
|
|
955
|
+
* `pg_log_error("\\%s not allowed in pipeline mode", cmd)` (note: no `:`
|
|
956
|
+
* after the command name — different shape from `errResult`).
|
|
957
|
+
*
|
|
958
|
+
* `\gdesc` is the odd one out: upstream uses
|
|
959
|
+
* `pg_log_error("synchronous command execution functions are not allowed in
|
|
960
|
+
* pipeline mode")` because the underlying `PQdescribePrepared`/`PQfn`-style
|
|
961
|
+
* helpers all share that text — the regress baseline asserts this exact
|
|
962
|
+
* wording at three call sites in `psql_pipeline.out`.
|
|
963
|
+
*
|
|
964
|
+
* If the command proceeded it would inject a synchronous Query/Sync into the
|
|
965
|
+
* queue, corrupt the pipeline state, and leave `\endpipeline` waiting forever.
|
|
966
|
+
*
|
|
967
|
+
* Returns `null` when not in pipeline mode (caller proceeds); otherwise
|
|
968
|
+
* returns a populated error result the caller should bubble up.
|
|
969
|
+
*
|
|
970
|
+
* Upstream psql 18.4 leaks gate diagnostics through `pg_log_error_internal`
|
|
971
|
+
* which appends to the libpq result error log on the underlying PGresult;
|
|
972
|
+
* each subsequent gate hit RE-EMITS the full accumulated log plus its own
|
|
973
|
+
* line ("Error messages accumulate and are repeated" — the regress comment
|
|
974
|
+
* is the spec). Two `\gdesc` calls back-to-back therefore emit 3 lines
|
|
975
|
+
* total: 1 for the first call, 2 for the second (one accumulated + one
|
|
976
|
+
* own). Mirror that with a settings-stashed accumulator keyed off the
|
|
977
|
+
* current pipeline session; reset when the pipeline ends.
|
|
978
|
+
*/
|
|
979
|
+
const PIPELINE_GATE_ERRORS_KEY = Symbol.for("neonctl.psql.pipelineGateErrors");
|
|
980
|
+
const getGateErrors = (settings) => {
|
|
981
|
+
const s = settings;
|
|
982
|
+
let cur = s[PIPELINE_GATE_ERRORS_KEY];
|
|
983
|
+
if (!cur) {
|
|
984
|
+
cur = [];
|
|
985
|
+
s[PIPELINE_GATE_ERRORS_KEY] = cur;
|
|
986
|
+
}
|
|
987
|
+
return cur;
|
|
988
|
+
};
|
|
989
|
+
/**
|
|
990
|
+
* Drop the accumulated pipeline-gate errors. Called from
|
|
991
|
+
* `\endpipeline` so the next pipeline session starts fresh — without
|
|
992
|
+
* this, gate errors from a closed pipeline would leak into the next
|
|
993
|
+
* one.
|
|
994
|
+
*/
|
|
995
|
+
const clearPipelineGateErrors = (settings) => {
|
|
996
|
+
const s = settings;
|
|
997
|
+
s[PIPELINE_GATE_ERRORS_KEY] = void 0;
|
|
998
|
+
};
|
|
999
|
+
const pipelineGate = (ctx) => {
|
|
1000
|
+
if (ctx.settings.sendMode !== "extended-pipeline") return null;
|
|
1001
|
+
const message = ctx.cmdName === "gdesc" ? "synchronous command execution functions are not allowed in pipeline mode" : `\\${ctx.cmdName} not allowed in pipeline mode`;
|
|
1002
|
+
ctx.settings.lastErrorResult = { message };
|
|
1003
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
1004
|
+
if (ctx.cmdName === "gdesc") {
|
|
1005
|
+
const log = getGateErrors(ctx.settings);
|
|
1006
|
+
log.push(message);
|
|
1007
|
+
for (const m of log) writeErr(`${prefix}${m}\n`);
|
|
1008
|
+
} else writeErr(`${prefix}${message}\n`);
|
|
1009
|
+
return {
|
|
1010
|
+
status: "error",
|
|
1011
|
+
errorWritten: true
|
|
1012
|
+
};
|
|
1013
|
+
};
|
|
1014
|
+
/**
|
|
1015
|
+
* Set of psql variables upstream marks as "specially treated" — i.e. names
|
|
1016
|
+
* that have a substitute / assign hook installed in `startup.c`'s
|
|
1017
|
+
* `EstablishVariableSpace`. Used by `\gset` to reject assignments into
|
|
1018
|
+
* those names (matching upstream `StoreQueryTuple`'s `VariableHasHook`
|
|
1019
|
+
* check). We mirror the upstream list directly so a `\gset IGNORE` into
|
|
1020
|
+
* `IGNOREEOF` produces the conformance-expected warning even though our
|
|
1021
|
+
* settings.ts hasn't installed the IGNOREEOF / HISTFILE hooks yet — that
|
|
1022
|
+
* gap is tracked separately and harmless because the values are read-only
|
|
1023
|
+
* for us.
|
|
1024
|
+
*/
|
|
1025
|
+
const UPSTREAM_SPECIAL_VAR_NAMES = /* @__PURE__ */ new Set([
|
|
1026
|
+
"AUTOCOMMIT",
|
|
1027
|
+
"COMP_KEYWORD_CASE",
|
|
1028
|
+
"ECHO",
|
|
1029
|
+
"ECHO_HIDDEN",
|
|
1030
|
+
"FETCH_COUNT",
|
|
1031
|
+
"HIDE_TABLEAM",
|
|
1032
|
+
"HIDE_TOAST_COMPRESSION",
|
|
1033
|
+
"HISTCONTROL",
|
|
1034
|
+
"HISTFILE",
|
|
1035
|
+
"HISTSIZE",
|
|
1036
|
+
"IGNOREEOF",
|
|
1037
|
+
"ON_ERROR_ROLLBACK",
|
|
1038
|
+
"ON_ERROR_STOP",
|
|
1039
|
+
"PROMPT1",
|
|
1040
|
+
"PROMPT2",
|
|
1041
|
+
"PROMPT3",
|
|
1042
|
+
"QUIET",
|
|
1043
|
+
"SHOW_ALL_RESULTS",
|
|
1044
|
+
"SHOW_CONTEXT",
|
|
1045
|
+
"SINGLELINE",
|
|
1046
|
+
"SINGLESTEP",
|
|
1047
|
+
"VERBOSITY"
|
|
1048
|
+
]);
|
|
1049
|
+
/**
|
|
1050
|
+
* True when `name` is a psql variable that `\gset` must skip with an
|
|
1051
|
+
* "attempt to \gset into specially treated variable" message. Combines the
|
|
1052
|
+
* registered-hook check (so future hook installations are automatically
|
|
1053
|
+
* covered) with the upstream-canonical list above (so cases like
|
|
1054
|
+
* IGNOREEOF that aren't hooked in our settings.ts still match upstream's
|
|
1055
|
+
* `\gset` behaviour exactly).
|
|
1056
|
+
*/
|
|
1057
|
+
const isSpeciallyTreatedVar = (settings, name) => settings.vars.hasSubstituteHook(name) || UPSTREAM_SPECIAL_VAR_NAMES.has(name);
|
|
1058
|
+
/**
|
|
1059
|
+
* Strip line and block comments from `sql` so a COPY-shaped token inside a
|
|
1060
|
+
* comment (e.g. dash-dash `COPY t TO STDOUT`) doesn't trigger the
|
|
1061
|
+
* `\g FILE` mid-batch sink. Mirrors the cheap normaliser the mainloop uses
|
|
1062
|
+
* before wiring `copyOutMidBatchSink`. Embedded literals are NOT stripped —
|
|
1063
|
+
* `'COPY x TO STDOUT'` would still match `hasCopyToStdout`, but that's the
|
|
1064
|
+
* same false-positive shape upstream tolerates (the regex sweep is
|
|
1065
|
+
* intentionally conservative, and the worst-case outcome is "route bytes
|
|
1066
|
+
* that never arrive to the file" — harmless).
|
|
1067
|
+
*/
|
|
1068
|
+
const stripSqlCommentsForCopyScan = (sql) => sql.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/--[^\n]*/gu, "");
|
|
1069
|
+
/**
|
|
1070
|
+
* True when `sql` contains at least one `COPY ... TO STDOUT` segment.
|
|
1071
|
+
* Used by `runGCore` to install a CopyData sink while `\g` / `\gx` /
|
|
1072
|
+
* `\g FILE` / `\g |cmd` dispatches a `\;`-chained batch that mixes
|
|
1073
|
+
* COPY-OUT with regular SELECT statements. Without the sink the wire
|
|
1074
|
+
* layer drops the CopyData bytes on the floor, and the file/pipe ends
|
|
1075
|
+
* up with the surrounding tuple results only — see regress/psql lines
|
|
1076
|
+
* 5760-5787 (`COPY (SELECT 'foo') TO STDOUT \; COPY (SELECT 'bar') TO
|
|
1077
|
+
* STDOUT \g :g_out_file`).
|
|
1078
|
+
*/
|
|
1079
|
+
const hasCopyToStdout = (sql) => /\bCOPY\b[\s\S]*?\bTO\s+STDOUT\b/iu.test(stripSqlCommentsForCopyScan(sql));
|
|
1080
|
+
/**
|
|
1081
|
+
* Render a server-side error in upstream psql's 3-line shape (severity +
|
|
1082
|
+
* message, then `LINE N:` / `^` re-print) and refresh the `:LAST_ERROR_*`
|
|
1083
|
+
* diagnostic variables so a subsequent `\errverbose` sees the rich payload.
|
|
1084
|
+
*
|
|
1085
|
+
* Mirrors the path that `core/common.ts::writeQueryError` takes for top-level
|
|
1086
|
+
* statement errors: capture full ErrorResponse fields onto
|
|
1087
|
+
* `settings.lastErrorResult`, render via `formatErrorReport` (honouring
|
|
1088
|
+
* VERBOSITY + SHOW_CONTEXT), prefix only the leading severity line with
|
|
1089
|
+
* `psql:[<file>:<n>]:`, and update the per-statement diagnostic vars via
|
|
1090
|
+
* `refreshErrorVars`.
|
|
1091
|
+
*
|
|
1092
|
+
* Used by `\g`, `\gx`, `\gset`, `\gdesc`, and `\gexec` so a server-rejected
|
|
1093
|
+
* statement dispatched through them renders the same shape vanilla psql
|
|
1094
|
+
* produces, instead of the legacy `\<cmd>: <message>` one-liner.
|
|
1095
|
+
*/
|
|
1096
|
+
const formatServerError = (ctx, err, sql) => {
|
|
1097
|
+
const msg = captureLastError(ctx.settings, err, sql);
|
|
1098
|
+
const e = ctx.settings.lastErrorResult;
|
|
1099
|
+
if (e) {
|
|
1100
|
+
const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
|
|
1101
|
+
const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
|
|
1102
|
+
writeErr(prefixed.join("\n") + "\n");
|
|
1103
|
+
} else {
|
|
1104
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
1105
|
+
writeErr(`${prefix}ERROR: ${msg}\n`);
|
|
1106
|
+
}
|
|
1107
|
+
refreshErrorVars(ctx.settings, { kind: "error" });
|
|
1108
|
+
return {
|
|
1109
|
+
status: "error",
|
|
1110
|
+
errorWritten: true
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
/**
|
|
1114
|
+
* Open a writable destination for `\o` / `\w` / `\g FILE` / `\g |cmd`.
|
|
1115
|
+
*
|
|
1116
|
+
* `target` of the form `|cmd` spawns `sh -c cmd` and pipes to its stdin.
|
|
1117
|
+
* The returned closer waits for the child to exit and resolves to its
|
|
1118
|
+
* status + terminating signal (if any) so callers can render
|
|
1119
|
+
* `wait_result_to_str`-style errors. Any other string is treated as a
|
|
1120
|
+
* file path; the file is truncated.
|
|
1121
|
+
*/
|
|
1122
|
+
const openWriter = (target) => {
|
|
1123
|
+
if (target.startsWith("|")) {
|
|
1124
|
+
const cmd = target.slice(1);
|
|
1125
|
+
const child = spawn("sh", ["-c", cmd], { stdio: [
|
|
1126
|
+
"pipe",
|
|
1127
|
+
"inherit",
|
|
1128
|
+
"inherit"
|
|
1129
|
+
] });
|
|
1130
|
+
child.stdin.on("error", (err) => {
|
|
1131
|
+
if (err.code !== "EPIPE") throw err;
|
|
1132
|
+
});
|
|
1133
|
+
return {
|
|
1134
|
+
stream: child.stdin,
|
|
1135
|
+
isPipe: true,
|
|
1136
|
+
close: () => new Promise((resolve) => {
|
|
1137
|
+
let settled = false;
|
|
1138
|
+
const finish = (code, signal) => {
|
|
1139
|
+
if (settled) return;
|
|
1140
|
+
settled = true;
|
|
1141
|
+
resolve({
|
|
1142
|
+
exitCode: code,
|
|
1143
|
+
signal
|
|
1144
|
+
});
|
|
1145
|
+
};
|
|
1146
|
+
child.once("close", (code, signal) => {
|
|
1147
|
+
finish(code, signal);
|
|
1148
|
+
});
|
|
1149
|
+
child.once("error", () => {
|
|
1150
|
+
finish(127, null);
|
|
1151
|
+
});
|
|
1152
|
+
if (!child.stdin.destroyed) child.stdin.end();
|
|
1153
|
+
})
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
const fd = openSync(target, "w");
|
|
1157
|
+
const stream = createWriteStream(target, {
|
|
1158
|
+
encoding: "utf8",
|
|
1159
|
+
fd,
|
|
1160
|
+
autoClose: false
|
|
1161
|
+
});
|
|
1162
|
+
let writeError = null;
|
|
1163
|
+
stream.on("error", (err) => {
|
|
1164
|
+
writeError = writeError ?? err;
|
|
1165
|
+
});
|
|
1166
|
+
return {
|
|
1167
|
+
stream,
|
|
1168
|
+
isPipe: false,
|
|
1169
|
+
close: () => new Promise((resolve, reject) => {
|
|
1170
|
+
if (writeError !== null) {
|
|
1171
|
+
try {
|
|
1172
|
+
closeSync(fd);
|
|
1173
|
+
} catch {}
|
|
1174
|
+
reject(writeError);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
stream.end((err) => {
|
|
1178
|
+
if (err) {
|
|
1179
|
+
try {
|
|
1180
|
+
closeSync(fd);
|
|
1181
|
+
} catch {}
|
|
1182
|
+
reject(err);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
fsyncSync(fd);
|
|
1187
|
+
} catch {}
|
|
1188
|
+
try {
|
|
1189
|
+
closeSync(fd);
|
|
1190
|
+
} catch (closeErr) {
|
|
1191
|
+
reject(closeErr);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (platform() === "darwin") {
|
|
1195
|
+
setTimeout(() => {
|
|
1196
|
+
resolve({});
|
|
1197
|
+
}, 25);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
resolve({});
|
|
1201
|
+
});
|
|
1202
|
+
})
|
|
1203
|
+
};
|
|
1204
|
+
};
|
|
1205
|
+
/**
|
|
1206
|
+
* Map a Node.js errno (`err.code`) to the libc `strerror()` string
|
|
1207
|
+
* upstream psql renders in its `pg_log_error("%s: %m", fname)` path.
|
|
1208
|
+
*
|
|
1209
|
+
* Falls back to `err.message` (with the verbose `ENOENT: ...` prefix
|
|
1210
|
+
* stripped if present) so unmapped errno values still surface
|
|
1211
|
+
* meaningful text instead of a cryptic Node-internal phrasing.
|
|
1212
|
+
*/
|
|
1213
|
+
const errnoToStrerror = (err) => {
|
|
1214
|
+
switch (err.code) {
|
|
1215
|
+
case "ENOENT": return "No such file or directory";
|
|
1216
|
+
case "EACCES": return "Permission denied";
|
|
1217
|
+
case "EISDIR": return "Is a directory";
|
|
1218
|
+
case "ENOTDIR": return "Not a directory";
|
|
1219
|
+
case "EEXIST": return "File exists";
|
|
1220
|
+
case "EROFS": return "Read-only file system";
|
|
1221
|
+
case "ELOOP": return "Too many levels of symbolic links";
|
|
1222
|
+
case "ENAMETOOLONG": return "File name too long";
|
|
1223
|
+
case "ENOSPC": return "No space left on device";
|
|
1224
|
+
case "EMFILE": return "Too many open files";
|
|
1225
|
+
case "ENFILE": return "Too many open files in system";
|
|
1226
|
+
case "EIO": return "Input/output error";
|
|
1227
|
+
case "EFBIG": return "File too large";
|
|
1228
|
+
case "EDQUOT": return "Disk quota exceeded";
|
|
1229
|
+
case "EPERM": return "Operation not permitted";
|
|
1230
|
+
case "EINVAL": return "Invalid argument";
|
|
1231
|
+
default: {
|
|
1232
|
+
const m = /^[A-Z]+: ([^,]+)/.exec(err.message);
|
|
1233
|
+
return m ? m[1] : err.message;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
/**
|
|
1238
|
+
* Emit a file-open failure for `\g FILE`, `\o FILE`, `\w FILE` in the
|
|
1239
|
+
* exact shape vanilla psql produces: a bare `<path>: <strerror>` line
|
|
1240
|
+
* on stderr, no `\<cmd>:` prefix (matches `pg_log_error` under terse
|
|
1241
|
+
* mode, which is what `psql -X` uses).
|
|
1242
|
+
*
|
|
1243
|
+
* The leading `psql:[<file>:<n>]:` tag is still applied when we're
|
|
1244
|
+
* reading SQL from a `\i FILE` include — `psqlErrorPrefix` returns ''
|
|
1245
|
+
* for stdin so the line stays bare for the interactive / harness case.
|
|
1246
|
+
*
|
|
1247
|
+
* Returns an `error` envelope with `errorWritten: true` so the mainloop
|
|
1248
|
+
* doesn't write a duplicate `psql: ERROR:` fallback.
|
|
1249
|
+
*/
|
|
1250
|
+
const reportFileOpenFailure = (ctx, target, err) => {
|
|
1251
|
+
const line = `${target}: ${errnoToStrerror(err)}`;
|
|
1252
|
+
ctx.settings.lastErrorResult = { message: line };
|
|
1253
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
1254
|
+
writeErr(`${prefix}${line}\n`);
|
|
1255
|
+
return {
|
|
1256
|
+
status: "error",
|
|
1257
|
+
errorWritten: true
|
|
1258
|
+
};
|
|
1259
|
+
};
|
|
1260
|
+
/**
|
|
1261
|
+
* True when `err` was thrown by our synchronous `openSync` in
|
|
1262
|
+
* {@link openWriter} (i.e. has an errno `code`) and the caller should
|
|
1263
|
+
* render it via {@link reportFileOpenFailure} rather than the generic
|
|
1264
|
+
* `\<cmd>: <msg>` path.
|
|
1265
|
+
*/
|
|
1266
|
+
const isFileOpenFailure = (err) => {
|
|
1267
|
+
if (!err || typeof err !== "object") return false;
|
|
1268
|
+
const e = err;
|
|
1269
|
+
return typeof e.code === "string" && e.code.startsWith("E");
|
|
1270
|
+
};
|
|
1271
|
+
/**
|
|
1272
|
+
* Format a child process exit code + signal into upstream psql's
|
|
1273
|
+
* `wait_result_to_str` style. Mirrors the C helper in
|
|
1274
|
+
* `src/common/wait_error.c`:
|
|
1275
|
+
*
|
|
1276
|
+
* - exit code 127 → `command not found`
|
|
1277
|
+
* - exit code 126 → `command was not executable`
|
|
1278
|
+
* - any other code → `child process exited with exit code N`
|
|
1279
|
+
* - terminated by signal S → `child process was terminated by
|
|
1280
|
+
* signal N: <SIG>`
|
|
1281
|
+
*
|
|
1282
|
+
* Returns null when the child exited cleanly (code 0, no signal).
|
|
1283
|
+
*/
|
|
1284
|
+
const formatChildWaitResult = (exitCode, signal) => {
|
|
1285
|
+
if (signal) return `child process was terminated by signal: ${signal}`;
|
|
1286
|
+
if (exitCode === null || exitCode === void 0) return null;
|
|
1287
|
+
if (exitCode === 0) return null;
|
|
1288
|
+
if (exitCode === 127) return "command not found";
|
|
1289
|
+
if (exitCode === 126) return "command was not executable";
|
|
1290
|
+
return `child process exited with exit code ${String(exitCode)}`;
|
|
1291
|
+
};
|
|
1292
|
+
/**
|
|
1293
|
+
* Compose the `CommandComplete`-tag line upstream prints for non-tuples-
|
|
1294
|
+
* producing results (DDL, DML without RETURNING, COPY). Mirrors
|
|
1295
|
+
* `formatCommandTag` in `core/common.ts` — duplicated to avoid the
|
|
1296
|
+
* cmd_io → common import cycle. Returns an empty string when no tag is
|
|
1297
|
+
* available (e.g. `EmptyQueryResponse` carries `command = ''`).
|
|
1298
|
+
*/
|
|
1299
|
+
const formatCommandTagText = (rs) => {
|
|
1300
|
+
const command = (rs.command || "").trim();
|
|
1301
|
+
if (command.length === 0) return "";
|
|
1302
|
+
if (command === "INSERT") return `INSERT ${String(rs.oid ?? 0)} ${String(rs.rowCount ?? 0)}`;
|
|
1303
|
+
if (rs.rowCount !== null && rs.rowCount !== void 0) return `${command} ${String(rs.rowCount)}`;
|
|
1304
|
+
return command;
|
|
1305
|
+
};
|
|
1306
|
+
/**
|
|
1307
|
+
* Render a `ResultSet` to the supplied writable stream using the printer
|
|
1308
|
+
* picked from the active `\pset format`. Upstream's `do_watch`, `do_gset`,
|
|
1309
|
+
* `do_gexec`, and `\i` all funnel results through the standard query
|
|
1310
|
+
* output pipeline (`ExecQueryAndProcessResults` → `printQuery`), which
|
|
1311
|
+
* honours `\pset format`. Hard-coding the aligned printer here breaks the
|
|
1312
|
+
* conformance harness's `psql -A` runs (which expect unaligned tuples-only
|
|
1313
|
+
* output for things like `\watch` polled rows).
|
|
1314
|
+
*
|
|
1315
|
+
* Non-tuples-producing results (CommandComplete with `fields.length === 0`,
|
|
1316
|
+
* which covers DDL, DML without RETURNING, and the post-CopyDone tag for
|
|
1317
|
+
* `COPY ... TO STDOUT`) are rendered as a single status line instead of
|
|
1318
|
+
* the printer's `(0 rows)` empty-table block — matching `renderResultSets`
|
|
1319
|
+
* in `core/common.ts`. Tuples-only (`\t`) and quiet (`--quiet`) both
|
|
1320
|
+
* suppress the tag entirely.
|
|
1321
|
+
*/
|
|
1322
|
+
const renderResult = async (settings, rs, out) => {
|
|
1323
|
+
if (rs.copyOutBytes && rs.copyOutBytes.length > 0) for (const chunk of rs.copyOutBytes) out.write(chunk);
|
|
1324
|
+
if (rs.fields.length === 0) {
|
|
1325
|
+
if (!settings.popt.topt.tuplesOnly && !settings.quiet && !rs.copyOutBytes) {
|
|
1326
|
+
const tag = formatCommandTagText(rs);
|
|
1327
|
+
if (tag.length > 0) out.write(`${tag}\n`);
|
|
1328
|
+
}
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
await pickActivePrinter(settings).printQuery(rs, settings.popt, out);
|
|
1332
|
+
};
|
|
1333
|
+
/**
|
|
1334
|
+
* Pick the printer for the active output format. Mirrors `pickPrinter`
|
|
1335
|
+
* in `core/common.ts` — duplicated here to avoid the cmd_io → common
|
|
1336
|
+
* import cycle (common.ts depends on this file for `getQueryFout`).
|
|
1337
|
+
*
|
|
1338
|
+
* `wrapped` falls back to the aligned printer (which renders `wrapped`
|
|
1339
|
+
* mode itself via `topt.format`).
|
|
1340
|
+
*/
|
|
1341
|
+
const pickActivePrinter = (settings) => {
|
|
1342
|
+
switch (settings.popt.topt.format) {
|
|
1343
|
+
case "aligned":
|
|
1344
|
+
case "wrapped": return alignedPrinter;
|
|
1345
|
+
case "unaligned": return unalignedPrinter;
|
|
1346
|
+
case "csv": return csvPrinter;
|
|
1347
|
+
case "json": return jsonPrinter;
|
|
1348
|
+
case "html": return htmlPrinter;
|
|
1349
|
+
case "asciidoc": return asciidocPrinter;
|
|
1350
|
+
case "latex": return latexPrinter;
|
|
1351
|
+
case "latex-longtable": return latexLongtablePrinter;
|
|
1352
|
+
case "troff-ms": return troffMsPrinter;
|
|
1353
|
+
default: return alignedPrinter;
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
/**
|
|
1357
|
+
* Pick the output target for a query result.
|
|
1358
|
+
*
|
|
1359
|
+
* Precedence: explicit `oneShot` (e.g. `\g FILE`) > the settings stash
|
|
1360
|
+
* (`\o FILE`) > `process.stdout`.
|
|
1361
|
+
*/
|
|
1362
|
+
const pickOut = (settings, oneShot) => {
|
|
1363
|
+
if (oneShot) return oneShot;
|
|
1364
|
+
return getQueryFout(settings) ?? process.stdout;
|
|
1365
|
+
};
|
|
1366
|
+
const runInclude = async (ctx, relative) => {
|
|
1367
|
+
const arg = ctx.nextArg("normal");
|
|
1368
|
+
if (arg === null || arg.length === 0) return errResult$1(ctx, "missing required argument");
|
|
1369
|
+
let resolved;
|
|
1370
|
+
if (path.isAbsolute(arg)) resolved = arg;
|
|
1371
|
+
else if (relative && ctx.settings.inputfile) resolved = path.resolve(path.dirname(ctx.settings.inputfile), arg);
|
|
1372
|
+
else resolved = path.resolve(process.cwd(), arg);
|
|
1373
|
+
let contents;
|
|
1374
|
+
try {
|
|
1375
|
+
contents = await promises.readFile(resolved, "utf8");
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1378
|
+
return errResult$1(ctx, msg);
|
|
1379
|
+
}
|
|
1380
|
+
if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
|
|
1381
|
+
const trimmed = contents.trim();
|
|
1382
|
+
if (trimmed.length === 0) return { status: "ok" };
|
|
1383
|
+
const priorInputFile = ctx.settings.inputfile;
|
|
1384
|
+
ctx.settings.inputfile = resolved;
|
|
1385
|
+
try {
|
|
1386
|
+
const results = await ctx.settings.db.execSimple(trimmed);
|
|
1387
|
+
const out = pickOut(ctx.settings, null);
|
|
1388
|
+
for (const rs of results) await renderResult(ctx.settings, rs, out);
|
|
1389
|
+
return { status: "ok" };
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1392
|
+
return errResult$1(ctx, msg);
|
|
1393
|
+
} finally {
|
|
1394
|
+
ctx.settings.inputfile = priorInputFile;
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
const cmdInclude = {
|
|
1398
|
+
name: "i",
|
|
1399
|
+
aliases: ["include"],
|
|
1400
|
+
helpKey: "i",
|
|
1401
|
+
run: (ctx) => runInclude(ctx, false)
|
|
1402
|
+
};
|
|
1403
|
+
const cmdIncludeRel = {
|
|
1404
|
+
name: "ir",
|
|
1405
|
+
aliases: ["include_relative"],
|
|
1406
|
+
helpKey: "ir",
|
|
1407
|
+
run: (ctx) => runInclude(ctx, true)
|
|
1408
|
+
};
|
|
1409
|
+
const cmdOut = {
|
|
1410
|
+
name: "o",
|
|
1411
|
+
aliases: ["out"],
|
|
1412
|
+
helpKey: "o",
|
|
1413
|
+
async run(ctx) {
|
|
1414
|
+
const arg = ctx.nextArg("filepipe");
|
|
1415
|
+
await closeQueryFout(ctx.settings);
|
|
1416
|
+
if (arg === null || arg.length === 0) return { status: "ok" };
|
|
1417
|
+
try {
|
|
1418
|
+
const entry = openWriter(arg);
|
|
1419
|
+
setQueryFout(ctx.settings, entry);
|
|
1420
|
+
return { status: "ok" };
|
|
1421
|
+
} catch (err) {
|
|
1422
|
+
if (!arg.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, arg, err);
|
|
1423
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1424
|
+
return errResult$1(ctx, msg);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
const cmdWrite = {
|
|
1429
|
+
name: "w",
|
|
1430
|
+
aliases: ["write"],
|
|
1431
|
+
helpKey: "w",
|
|
1432
|
+
async run(ctx) {
|
|
1433
|
+
const arg = ctx.nextArg("filepipe");
|
|
1434
|
+
if (arg === null || arg.length === 0) return errResult$1(ctx, "missing required argument");
|
|
1435
|
+
let entry;
|
|
1436
|
+
try {
|
|
1437
|
+
entry = openWriter(arg);
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
if (!arg.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, arg, err);
|
|
1440
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1441
|
+
return errResult$1(ctx, msg);
|
|
1442
|
+
}
|
|
1443
|
+
try {
|
|
1444
|
+
await new Promise((resolve, reject) => {
|
|
1445
|
+
entry.stream.write(ctx.queryBuf, (err) => {
|
|
1446
|
+
if (err) reject(err);
|
|
1447
|
+
else resolve();
|
|
1448
|
+
});
|
|
1449
|
+
});
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
const isEpipe = err instanceof Error && err.code === "EPIPE";
|
|
1452
|
+
if (!entry.isPipe || !isEpipe) {
|
|
1453
|
+
try {
|
|
1454
|
+
await entry.close();
|
|
1455
|
+
} catch {}
|
|
1456
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1457
|
+
return errResult$1(ctx, msg);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
try {
|
|
1461
|
+
const result = await entry.close();
|
|
1462
|
+
if (entry.isPipe) {
|
|
1463
|
+
const msg = formatChildWaitResult(result.exitCode, result.signal);
|
|
1464
|
+
if (msg !== null) {
|
|
1465
|
+
const line = `${arg}: ${msg}`;
|
|
1466
|
+
ctx.settings.lastErrorResult = { message: line };
|
|
1467
|
+
const prefix = psqlErrorPrefix(ctx.settings);
|
|
1468
|
+
writeErr(`${prefix}${line}\n`);
|
|
1469
|
+
return {
|
|
1470
|
+
status: "error",
|
|
1471
|
+
errorWritten: true
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return { status: "ok" };
|
|
1476
|
+
} catch (err) {
|
|
1477
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1478
|
+
return errResult$1(ctx, msg);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
/**
|
|
1483
|
+
* Parse the body of a `\g (option=value option2=value2 ...)` clause —
|
|
1484
|
+
* the text between the outer parentheses, already stripped. Options
|
|
1485
|
+
* are separated by whitespace; values may be single-quoted to embed
|
|
1486
|
+
* spaces. Unquoted values run to the next whitespace.
|
|
1487
|
+
*
|
|
1488
|
+
* Mirrors upstream's `parse_slash_pgopts_list`. We deliberately stay
|
|
1489
|
+
* narrow — the conformance corpus exercises `format=`, `csv_fieldsep=`,
|
|
1490
|
+
* and `title=` only.
|
|
1491
|
+
*/
|
|
1492
|
+
const parseGPsetOptions = (body) => {
|
|
1493
|
+
const out = [];
|
|
1494
|
+
let i = 0;
|
|
1495
|
+
while (i < body.length) {
|
|
1496
|
+
while (i < body.length && /\s/.test(body[i])) i++;
|
|
1497
|
+
if (i >= body.length) break;
|
|
1498
|
+
const optStart = i;
|
|
1499
|
+
while (i < body.length && body[i] !== "=" && !/\s/.test(body[i])) i++;
|
|
1500
|
+
const option = body.slice(optStart, i);
|
|
1501
|
+
if (option.length === 0) break;
|
|
1502
|
+
let value = "";
|
|
1503
|
+
if (body[i] === "=") {
|
|
1504
|
+
i++;
|
|
1505
|
+
if (body[i] === "'") {
|
|
1506
|
+
i++;
|
|
1507
|
+
while (i < body.length && body[i] !== "'") {
|
|
1508
|
+
if (body[i] === "\\" && i + 1 < body.length) {
|
|
1509
|
+
const next = body[i + 1];
|
|
1510
|
+
if (next === "n") value += "\n";
|
|
1511
|
+
else if (next === "t") value += " ";
|
|
1512
|
+
else if (next === "r") value += "\r";
|
|
1513
|
+
else if (next === "\\") value += "\\";
|
|
1514
|
+
else if (next === "'") value += "'";
|
|
1515
|
+
else value += next;
|
|
1516
|
+
i += 2;
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
value += body[i++];
|
|
1520
|
+
}
|
|
1521
|
+
if (body[i] === "'") i++;
|
|
1522
|
+
} else {
|
|
1523
|
+
const vStart = i;
|
|
1524
|
+
while (i < body.length && !/\s/.test(body[i])) i++;
|
|
1525
|
+
value = body.slice(vStart, i);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
out.push({
|
|
1529
|
+
option,
|
|
1530
|
+
value
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
return out;
|
|
1534
|
+
};
|
|
1535
|
+
const runGCore = async (ctx, forceExpanded) => {
|
|
1536
|
+
const gated = pipelineGate(ctx);
|
|
1537
|
+
if (gated !== null) return gated;
|
|
1538
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1539
|
+
const bufSql = trimmedBuf.trim();
|
|
1540
|
+
let target;
|
|
1541
|
+
let psetOverrides = null;
|
|
1542
|
+
const rawTrimmed = ctx.rawArgs.trimStart();
|
|
1543
|
+
if (rawTrimmed.startsWith("(")) {
|
|
1544
|
+
const close = rawTrimmed.indexOf(")");
|
|
1545
|
+
if (close === -1) return errResult$1(ctx, "missing right parenthesis in \\g options");
|
|
1546
|
+
psetOverrides = parseGPsetOptions(rawTrimmed.slice(1, close).trim());
|
|
1547
|
+
const afterParen = rawTrimmed.slice(close + 1).trim();
|
|
1548
|
+
target = afterParen.length > 0 ? afterParen : null;
|
|
1549
|
+
} else target = ctx.nextArg("filepipe");
|
|
1550
|
+
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery;
|
|
1551
|
+
const hasPendingNamedBind = stagedNamedBindPresent(ctx.settings);
|
|
1552
|
+
if (sql.length === 0 && !hasPendingNamedBind) return {
|
|
1553
|
+
status: "reset-buf",
|
|
1554
|
+
newBuf: ""
|
|
1555
|
+
};
|
|
1556
|
+
if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
|
|
1557
|
+
let oneShot = null;
|
|
1558
|
+
if (target !== null && target.length > 0) try {
|
|
1559
|
+
oneShot = openWriter(target);
|
|
1560
|
+
} catch (err) {
|
|
1561
|
+
if (!target.startsWith("|") && isFileOpenFailure(err)) return reportFileOpenFailure(ctx, target, err);
|
|
1562
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1563
|
+
return errResult$1(ctx, msg);
|
|
1564
|
+
}
|
|
1565
|
+
const topt = ctx.settings.popt.topt;
|
|
1566
|
+
const toptSnapshot = { ...topt };
|
|
1567
|
+
if (forceExpanded) topt.expanded = "on";
|
|
1568
|
+
if (psetOverrides) for (const { option, value } of psetOverrides) applyPset(topt, option, value, ctx.cmdName, true);
|
|
1569
|
+
ctx.settings.lastQuery = sql;
|
|
1570
|
+
const bindState = consumeBindState(ctx.settings);
|
|
1571
|
+
let execError = null;
|
|
1572
|
+
let copyOutSinkConn = null;
|
|
1573
|
+
try {
|
|
1574
|
+
const out = pickOut(ctx.settings, oneShot?.stream ?? null);
|
|
1575
|
+
if (bindState?.byName) {
|
|
1576
|
+
const ps = lookupPrepared(ctx.settings, bindState.name);
|
|
1577
|
+
if (!ps) execError = Object.assign(/* @__PURE__ */ new Error(`prepared statement "${bindState.name}" does not exist`), {
|
|
1578
|
+
severity: "ERROR",
|
|
1579
|
+
code: "26000"
|
|
1580
|
+
});
|
|
1581
|
+
else {
|
|
1582
|
+
const rs = await ps.bindAndExecute(bindState.values);
|
|
1583
|
+
await renderResult(ctx.settings, rs, out);
|
|
1584
|
+
}
|
|
1585
|
+
} else if (bindState) {
|
|
1586
|
+
const rs = await ctx.settings.db.query(sql, bindState.values);
|
|
1587
|
+
await renderResult(ctx.settings, rs, out);
|
|
1588
|
+
} else {
|
|
1589
|
+
if (hasCopyToStdout(sql)) {
|
|
1590
|
+
copyOutSinkConn = ctx.settings.db;
|
|
1591
|
+
copyOutSinkConn.copyOutMidBatchSink = (chunk) => {
|
|
1592
|
+
out.write(chunk);
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
const results = await ctx.settings.db.execSimple(sql);
|
|
1596
|
+
for (const rs of results) await renderResult(ctx.settings, rs, out);
|
|
1597
|
+
}
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
execError = err;
|
|
1600
|
+
} finally {
|
|
1601
|
+
Object.assign(topt, toptSnapshot);
|
|
1602
|
+
if (copyOutSinkConn) copyOutSinkConn.copyOutMidBatchSink = null;
|
|
1603
|
+
}
|
|
1604
|
+
let pipeError = null;
|
|
1605
|
+
if (oneShot) try {
|
|
1606
|
+
await oneShot.close();
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
pipeError = err instanceof Error ? err.message : String(err);
|
|
1609
|
+
}
|
|
1610
|
+
if (execError !== null) return formatServerError(ctx, execError, bufSql.length > 0 ? trimmedBuf : sql);
|
|
1611
|
+
if (pipeError !== null) return errResult$1(ctx, pipeError);
|
|
1612
|
+
return {
|
|
1613
|
+
status: "reset-buf",
|
|
1614
|
+
newBuf: ""
|
|
1615
|
+
};
|
|
1616
|
+
};
|
|
1617
|
+
const cmdG = {
|
|
1618
|
+
name: "g",
|
|
1619
|
+
helpKey: "g",
|
|
1620
|
+
run: (ctx) => runGCore(ctx, false)
|
|
1621
|
+
};
|
|
1622
|
+
const cmdGx = {
|
|
1623
|
+
name: "gx",
|
|
1624
|
+
helpKey: "gx",
|
|
1625
|
+
run: (ctx) => runGCore(ctx, true)
|
|
1626
|
+
};
|
|
1627
|
+
/**
|
|
1628
|
+
* `\p` / `\print` — print the query buffer the next `\g` would execute.
|
|
1629
|
+
*
|
|
1630
|
+
* Mirrors upstream `exec_command_print` in `src/bin/psql/command.c`:
|
|
1631
|
+
*
|
|
1632
|
+
* if (query_buf && query_buf->len > 0)
|
|
1633
|
+
* puts(query_buf->data);
|
|
1634
|
+
* else if (previous_buf && previous_buf->len > 0)
|
|
1635
|
+
* puts(previous_buf->data);
|
|
1636
|
+
* else if (!pset.quiet)
|
|
1637
|
+
* puts(_("Query buffer is empty."));
|
|
1638
|
+
*
|
|
1639
|
+
* Buffer-vs-previous-buffer precedence matters for the regress sequence:
|
|
1640
|
+
*
|
|
1641
|
+
* SELECT 1; -- executes, previous_buf := "SELECT 1;"
|
|
1642
|
+
* \p -- queryBuf empty → prints previous_buf
|
|
1643
|
+
* SELECT 2 \r -- queryBuf="SELECT 2 ", \r resets to "" without
|
|
1644
|
+
* -- touching previous_buf
|
|
1645
|
+
* \p -- queryBuf still empty → prints previous_buf
|
|
1646
|
+
* SELECT 3 \p -- queryBuf="SELECT 3 ", non-empty → prints queryBuf
|
|
1647
|
+
*
|
|
1648
|
+
* Implementation notes:
|
|
1649
|
+
*
|
|
1650
|
+
* - We use `settings.lastQuery` as the previous-buffer source. Upstream
|
|
1651
|
+
* tracks `previous_buf` independently of `pset.last_query`, but our
|
|
1652
|
+
* `lastQuery` is set at the exact same point upstream sets
|
|
1653
|
+
* `previous_buf` (the dispatch site in `SendQuery`-equivalent code paths
|
|
1654
|
+
* in `core/common.ts` and `cmd_io.ts`'s `\g` implementation), so the
|
|
1655
|
+
* semantics match for every shape exercised by the conformance corpus.
|
|
1656
|
+
* - We must NOT clear queryBuf — return `status: 'ok'` so the mainloop
|
|
1657
|
+
* leaves the buffer untouched. The user is inspecting, not executing.
|
|
1658
|
+
* - `puts()` appends a trailing newline. We use `writeOut` and append `\n`
|
|
1659
|
+
* explicitly to match.
|
|
1660
|
+
*/
|
|
1661
|
+
const cmdPrint = {
|
|
1662
|
+
name: "p",
|
|
1663
|
+
aliases: ["print"],
|
|
1664
|
+
helpKey: "p",
|
|
1665
|
+
run: (ctx) => {
|
|
1666
|
+
if (ctx.queryBuf.trim().length > 0) writeOut(`${ctx.queryBuf}\n`);
|
|
1667
|
+
else if (ctx.settings.lastQuery.length > 0) writeOut(`${ctx.settings.lastQuery}\n`);
|
|
1668
|
+
else if (!ctx.settings.quiet) writeOut("Query buffer is empty.\n");
|
|
1669
|
+
return Promise.resolve({ status: "ok" });
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
const formatCell = (value) => {
|
|
1673
|
+
if (value === null || value === void 0) return "";
|
|
1674
|
+
if (typeof value === "string") return value;
|
|
1675
|
+
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
1676
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
1677
|
+
try {
|
|
1678
|
+
return JSON.stringify(value);
|
|
1679
|
+
} catch {
|
|
1680
|
+
return "";
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
const cmdGset = {
|
|
1684
|
+
name: "gset",
|
|
1685
|
+
helpKey: "gset",
|
|
1686
|
+
async run(ctx) {
|
|
1687
|
+
const gated = pipelineGate(ctx);
|
|
1688
|
+
if (gated !== null) return gated;
|
|
1689
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1690
|
+
const bufSql = trimmedBuf.trim();
|
|
1691
|
+
const prefix = ctx.nextArg("normal") ?? "";
|
|
1692
|
+
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
|
|
1693
|
+
if (sql.length === 0) return {
|
|
1694
|
+
status: "reset-buf",
|
|
1695
|
+
newBuf: ""
|
|
1696
|
+
};
|
|
1697
|
+
if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
|
|
1698
|
+
ctx.settings.lastQuery = sql;
|
|
1699
|
+
let results;
|
|
1700
|
+
try {
|
|
1701
|
+
results = await ctx.settings.db.execSimple(sql);
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
return formatServerError(ctx, err, bufSql.length > 0 ? trimmedBuf : sql);
|
|
1704
|
+
}
|
|
1705
|
+
if (results.length > 1) {
|
|
1706
|
+
const out = pickOut(ctx.settings, null);
|
|
1707
|
+
for (let i = 0; i < results.length - 1; i++) await renderResult(ctx.settings, results[i], out);
|
|
1708
|
+
}
|
|
1709
|
+
const lastRs = results[results.length - 1];
|
|
1710
|
+
if (!lastRs || lastRs.fields.length === 0) return {
|
|
1711
|
+
status: "reset-buf",
|
|
1712
|
+
newBuf: ""
|
|
1713
|
+
};
|
|
1714
|
+
const rs = lastRs;
|
|
1715
|
+
if (rs.rows.length === 0) {
|
|
1716
|
+
ctx.settings.lastErrorResult = { message: "no rows returned for \\gset" };
|
|
1717
|
+
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1718
|
+
writeErr(`${errPrefix}no rows returned for \\gset\n`);
|
|
1719
|
+
return {
|
|
1720
|
+
status: "error",
|
|
1721
|
+
errorWritten: true
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
if (rs.rows.length > 1) {
|
|
1725
|
+
ctx.settings.lastErrorResult = { message: "more than one row returned for \\gset" };
|
|
1726
|
+
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1727
|
+
writeErr(`${errPrefix}more than one row returned for \\gset\n`);
|
|
1728
|
+
return {
|
|
1729
|
+
status: "error",
|
|
1730
|
+
errorWritten: true
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
const row = rs.rows[0];
|
|
1734
|
+
for (let i = 0; i < rs.fields.length; i++) {
|
|
1735
|
+
const fieldName = rs.fields[i].name;
|
|
1736
|
+
const name = `${prefix}${fieldName}`;
|
|
1737
|
+
const cell = row[i];
|
|
1738
|
+
const isNull = cell === null || cell === void 0;
|
|
1739
|
+
if (isSpeciallyTreatedVar(ctx.settings, name)) {
|
|
1740
|
+
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1741
|
+
writeErr(`${errPrefix}attempt to \\gset into specially treated variable "${name}" ignored\n`);
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
if (isNull) {
|
|
1745
|
+
ctx.settings.vars.unset(name);
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
const value = formatCell(cell);
|
|
1749
|
+
if (!ctx.settings.vars.set(name, value)) {
|
|
1750
|
+
ctx.settings.lastErrorResult = { message: `invalid variable name: "${fieldName}"` };
|
|
1751
|
+
const errPrefix = psqlErrorPrefix(ctx.settings);
|
|
1752
|
+
writeErr(`${errPrefix}invalid variable name: "${fieldName}"\n`);
|
|
1753
|
+
return {
|
|
1754
|
+
status: "error",
|
|
1755
|
+
errorWritten: true
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
status: "reset-buf",
|
|
1761
|
+
newBuf: ""
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
/**
|
|
1766
|
+
* Build the SQL that resolves each describe-result column's `Type` via
|
|
1767
|
+
* `pg_catalog.format_type(typoid, typmod)`. We feed the names + OIDs
|
|
1768
|
+
* + typmods through a `VALUES` literal so the server does the formatting
|
|
1769
|
+
* for us — the same query upstream issues from `describeFieldsByType`.
|
|
1770
|
+
*
|
|
1771
|
+
* Returns null when there are zero fields (caller emits `(0 rows)` form
|
|
1772
|
+
* by hand because PostgreSQL rejects an empty VALUES list).
|
|
1773
|
+
*/
|
|
1774
|
+
const buildGdescFormatQuery = (fields) => {
|
|
1775
|
+
if (fields.length === 0) return null;
|
|
1776
|
+
return `SELECT name AS "Column", pg_catalog.format_type(tp, tpm) AS "Type" FROM (VALUES ${fields.map((f, i) => {
|
|
1777
|
+
const safeName = f.name.replace(/'/gu, "''");
|
|
1778
|
+
const oid = String(f.dataTypeID >>> 0);
|
|
1779
|
+
const typmod = String(f.dataTypeModifier | 0);
|
|
1780
|
+
return `(${String(i)}, '${safeName}', ${oid}::oid, ${typmod}::int4)`;
|
|
1781
|
+
}).join(", ")}) AS x(_idx, name, tp, tpm) ORDER BY _idx`;
|
|
1782
|
+
};
|
|
1783
|
+
/**
|
|
1784
|
+
* Field descriptors for the synthetic `Column / Type` ResultSet that
|
|
1785
|
+
* `\gdesc` emits when format_type resolution fails or yields nothing.
|
|
1786
|
+
*
|
|
1787
|
+
* We fall back to the field's raw OID so the user still sees a value.
|
|
1788
|
+
*/
|
|
1789
|
+
const GDESC_SYNTHETIC_FIELDS = [{
|
|
1790
|
+
name: "Column",
|
|
1791
|
+
tableID: 0,
|
|
1792
|
+
columnID: 0,
|
|
1793
|
+
dataTypeID: 25,
|
|
1794
|
+
dataTypeSize: -1,
|
|
1795
|
+
dataTypeModifier: -1,
|
|
1796
|
+
format: 0
|
|
1797
|
+
}, {
|
|
1798
|
+
name: "Type",
|
|
1799
|
+
tableID: 0,
|
|
1800
|
+
columnID: 0,
|
|
1801
|
+
dataTypeID: 25,
|
|
1802
|
+
dataTypeSize: -1,
|
|
1803
|
+
dataTypeModifier: -1,
|
|
1804
|
+
format: 0
|
|
1805
|
+
}];
|
|
1806
|
+
const buildSyntheticGdescResultSet = (rows) => ({
|
|
1807
|
+
command: "SELECT",
|
|
1808
|
+
rowCount: rows.length,
|
|
1809
|
+
oid: null,
|
|
1810
|
+
fields: GDESC_SYNTHETIC_FIELDS,
|
|
1811
|
+
rows,
|
|
1812
|
+
notices: []
|
|
1813
|
+
});
|
|
1814
|
+
const cmdGdesc = {
|
|
1815
|
+
name: "gdesc",
|
|
1816
|
+
helpKey: "gdesc",
|
|
1817
|
+
async run(ctx) {
|
|
1818
|
+
const gated = pipelineGate(ctx);
|
|
1819
|
+
if (gated !== null) return gated;
|
|
1820
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1821
|
+
const sql = trimmedBuf.trim();
|
|
1822
|
+
if (sql.length === 0) {
|
|
1823
|
+
process.stdout.write("The command has no result, or the result has no columns.\n");
|
|
1824
|
+
refreshErrorVars(ctx.settings, {
|
|
1825
|
+
kind: "success",
|
|
1826
|
+
rowCount: 0
|
|
1827
|
+
});
|
|
1828
|
+
return {
|
|
1829
|
+
status: "reset-buf",
|
|
1830
|
+
newBuf: ""
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
|
|
1834
|
+
ctx.settings.lastQuery = sql;
|
|
1835
|
+
let fields;
|
|
1836
|
+
try {
|
|
1837
|
+
const stmt = await ctx.settings.db.prepare("", sql);
|
|
1838
|
+
fields = await stmt.describe();
|
|
1839
|
+
try {
|
|
1840
|
+
await stmt.close();
|
|
1841
|
+
} catch {}
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
return formatServerError(ctx, err, trimmedBuf);
|
|
1844
|
+
}
|
|
1845
|
+
if (fields.length === 0) {
|
|
1846
|
+
process.stdout.write("The command has no result, or the result has no columns.\n");
|
|
1847
|
+
refreshErrorVars(ctx.settings, {
|
|
1848
|
+
kind: "success",
|
|
1849
|
+
rowCount: 0
|
|
1850
|
+
});
|
|
1851
|
+
return {
|
|
1852
|
+
status: "reset-buf",
|
|
1853
|
+
newBuf: ""
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
let rows;
|
|
1857
|
+
const formatQuery = buildGdescFormatQuery(fields);
|
|
1858
|
+
if (formatQuery === null) rows = [];
|
|
1859
|
+
else {
|
|
1860
|
+
const fallbackRows = () => fields.map((f) => [f.name, String(f.dataTypeID)]);
|
|
1861
|
+
try {
|
|
1862
|
+
const sets = await ctx.settings.db.execSimple(formatQuery);
|
|
1863
|
+
const last = sets[sets.length - 1];
|
|
1864
|
+
rows = last && last.rows.length > 0 ? last.rows : fallbackRows();
|
|
1865
|
+
} catch {
|
|
1866
|
+
rows = fallbackRows();
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
const rs = buildSyntheticGdescResultSet(rows);
|
|
1870
|
+
const printer = pickActivePrinter(ctx.settings);
|
|
1871
|
+
const out = pickOut(ctx.settings, null);
|
|
1872
|
+
try {
|
|
1873
|
+
await printer.printQuery(rs, ctx.settings.popt, out);
|
|
1874
|
+
} catch (err) {
|
|
1875
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1876
|
+
return errResult$1(ctx, msg);
|
|
1877
|
+
}
|
|
1878
|
+
refreshErrorVars(ctx.settings, {
|
|
1879
|
+
kind: "success",
|
|
1880
|
+
rowCount: rs.rowCount
|
|
1881
|
+
});
|
|
1882
|
+
return {
|
|
1883
|
+
status: "reset-buf",
|
|
1884
|
+
newBuf: ""
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
const cmdGexec = {
|
|
1889
|
+
name: "gexec",
|
|
1890
|
+
helpKey: "gexec",
|
|
1891
|
+
async run(ctx) {
|
|
1892
|
+
const gated = pipelineGate(ctx);
|
|
1893
|
+
if (gated !== null) return gated;
|
|
1894
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
1895
|
+
const bufSql = trimmedBuf.trim();
|
|
1896
|
+
const sql = bufSql.length > 0 ? bufSql : ctx.settings.lastQuery.trim();
|
|
1897
|
+
if (sql.length === 0) return {
|
|
1898
|
+
status: "reset-buf",
|
|
1899
|
+
newBuf: ""
|
|
1900
|
+
};
|
|
1901
|
+
if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
|
|
1902
|
+
ctx.settings.lastQuery = sql;
|
|
1903
|
+
let firstPass;
|
|
1904
|
+
try {
|
|
1905
|
+
firstPass = await ctx.settings.db.execSimple(sql);
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
return formatServerError(ctx, err, trimmedBuf);
|
|
1908
|
+
}
|
|
1909
|
+
const tupled = firstPass.filter((r) => r.fields.length > 0);
|
|
1910
|
+
if (tupled.length === 0) return {
|
|
1911
|
+
status: "reset-buf",
|
|
1912
|
+
newBuf: ""
|
|
1913
|
+
};
|
|
1914
|
+
const out = pickOut(ctx.settings, null);
|
|
1915
|
+
const echo = ctx.settings.echo;
|
|
1916
|
+
const shouldEcho = echo === "all" || echo === "queries";
|
|
1917
|
+
for (const rs of tupled) for (const row of rs.rows) for (const cell of row) {
|
|
1918
|
+
if (cell === null || cell === void 0) continue;
|
|
1919
|
+
const statement = formatCell(cell).trim();
|
|
1920
|
+
if (statement.length === 0) continue;
|
|
1921
|
+
if (shouldEcho) out.write(statement + "\n");
|
|
1922
|
+
try {
|
|
1923
|
+
const nested = await ctx.settings.db.execSimple(statement);
|
|
1924
|
+
for (const sub of nested) if (sub.fields.length > 0) await renderResult(ctx.settings, sub, out);
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
formatServerError(ctx, err, statement);
|
|
1927
|
+
if (ctx.settings.onErrorStop) return {
|
|
1928
|
+
status: "error",
|
|
1929
|
+
errorWritten: true
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return {
|
|
1934
|
+
status: "reset-buf",
|
|
1935
|
+
newBuf: ""
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
const sleepCancellable = (ms, signal) => new Promise((resolve) => {
|
|
1940
|
+
const timer = setTimeout(() => {
|
|
1941
|
+
signal.removeEventListener("abort", onAbort);
|
|
1942
|
+
resolve();
|
|
1943
|
+
}, ms);
|
|
1944
|
+
const onAbort = () => {
|
|
1945
|
+
clearTimeout(timer);
|
|
1946
|
+
signal.removeEventListener("abort", onAbort);
|
|
1947
|
+
resolve();
|
|
1948
|
+
};
|
|
1949
|
+
if (signal.aborted) {
|
|
1950
|
+
clearTimeout(timer);
|
|
1951
|
+
resolve();
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
signal.addEventListener("abort", onAbort);
|
|
1955
|
+
});
|
|
1956
|
+
/**
|
|
1957
|
+
* Strictly parse a non-negative finite float.
|
|
1958
|
+
*
|
|
1959
|
+
* Returns the parsed number, or `null` for any of:
|
|
1960
|
+
* - empty string
|
|
1961
|
+
* - non-numeric trailing characters (e.g. `10ab`)
|
|
1962
|
+
* - negative values (e.g. `-10`)
|
|
1963
|
+
* - out-of-range / non-finite results (e.g. `10e400` → Infinity)
|
|
1964
|
+
*
|
|
1965
|
+
* Used to validate `\watch` intervals and the `WATCH_INTERVAL` variable.
|
|
1966
|
+
*/
|
|
1967
|
+
const parseStrictNonNegativeFloat = (raw) => {
|
|
1968
|
+
if (raw.length === 0) return null;
|
|
1969
|
+
if (!/^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/.test(raw)) return null;
|
|
1970
|
+
const value = parseFloat(raw);
|
|
1971
|
+
if (!Number.isFinite(value)) return null;
|
|
1972
|
+
if (value < 0) return null;
|
|
1973
|
+
return value;
|
|
1974
|
+
};
|
|
1975
|
+
/**
|
|
1976
|
+
* Parse a strict non-negative integer (no exponent, no fractional).
|
|
1977
|
+
* Used for `c=` and `m=` / `min_rows=` argument values.
|
|
1978
|
+
*/
|
|
1979
|
+
const parseStrictNonNegativeInt = (raw) => {
|
|
1980
|
+
if (raw.length === 0) return null;
|
|
1981
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
1982
|
+
const value = parseInt(raw, 10);
|
|
1983
|
+
if (!Number.isFinite(value)) return null;
|
|
1984
|
+
return value;
|
|
1985
|
+
};
|
|
1986
|
+
/**
|
|
1987
|
+
* Default `\watch` interval (seconds). Mirrors upstream
|
|
1988
|
+
* `DEFAULT_WATCH_INTERVAL`. Exported so `defaultSettings` can substitute
|
|
1989
|
+
* it when the user unsets the `WATCH_INTERVAL` variable — upstream's
|
|
1990
|
+
* `watch_interval_substitute_hook` reseeds the value to `2` on null.
|
|
1991
|
+
*/
|
|
1992
|
+
const DEFAULT_WATCH_INTERVAL = "2";
|
|
1993
|
+
/**
|
|
1994
|
+
* Render `\watch`'s per-iteration timestamp in upstream psql's
|
|
1995
|
+
* `ctime`-style layout: `Day Mon DD HH:MM:SS YYYY` (e.g. `Mon May 25
|
|
1996
|
+
* 19:41:55 2026`). Upstream calls `strftime("%c", &tm)` with the C locale;
|
|
1997
|
+
* we reproduce the field order in vanilla English so the output matches
|
|
1998
|
+
* regardless of the host locale.
|
|
1999
|
+
*
|
|
2000
|
+
* Exported only for unit-testing the format ladder.
|
|
2001
|
+
*/
|
|
2002
|
+
const WEEKDAYS = [
|
|
2003
|
+
"Sun",
|
|
2004
|
+
"Mon",
|
|
2005
|
+
"Tue",
|
|
2006
|
+
"Wed",
|
|
2007
|
+
"Thu",
|
|
2008
|
+
"Fri",
|
|
2009
|
+
"Sat"
|
|
2010
|
+
];
|
|
2011
|
+
const MONTHS = [
|
|
2012
|
+
"Jan",
|
|
2013
|
+
"Feb",
|
|
2014
|
+
"Mar",
|
|
2015
|
+
"Apr",
|
|
2016
|
+
"May",
|
|
2017
|
+
"Jun",
|
|
2018
|
+
"Jul",
|
|
2019
|
+
"Aug",
|
|
2020
|
+
"Sep",
|
|
2021
|
+
"Oct",
|
|
2022
|
+
"Nov",
|
|
2023
|
+
"Dec"
|
|
2024
|
+
];
|
|
2025
|
+
const pad2 = (n) => n < 10 ? `0${String(n)}` : String(n);
|
|
2026
|
+
const formatWatchTimestamp = (now) => {
|
|
2027
|
+
return `${WEEKDAYS[now.getDay()]} ${MONTHS[now.getMonth()]} ${pad2(now.getDate())} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())} ${String(now.getFullYear())}`;
|
|
2028
|
+
};
|
|
2029
|
+
/**
|
|
2030
|
+
* Upper bound on the `WATCH_INTERVAL` variable and the positional interval
|
|
2031
|
+
* — matches upstream which rejects "out of range" values. Upstream uses
|
|
2032
|
+
* `strtod` and rejects ±Infinity; we tighten further so a single watch loop
|
|
2033
|
+
* cannot sleep for longer than ~100 hours, which catches obvious typos
|
|
2034
|
+
* without breaking legitimate slow polls.
|
|
2035
|
+
*/
|
|
2036
|
+
const WATCH_INTERVAL_MAX_SECONDS = 36e4;
|
|
2037
|
+
/**
|
|
2038
|
+
* Resolve the effective default `\watch` interval from the `WATCH_INTERVAL`
|
|
2039
|
+
* psql variable. Returns the parsed value, the documented default
|
|
2040
|
+
* (`DEFAULT_WATCH_INTERVAL`), or an `error` envelope if the variable is set
|
|
2041
|
+
* but parses out of range.
|
|
2042
|
+
*/
|
|
2043
|
+
const resolveWatchIntervalDefault = (settings) => {
|
|
2044
|
+
const raw = settings.vars.get("WATCH_INTERVAL") ?? "2";
|
|
2045
|
+
const parsed = parseStrictNonNegativeFloat(raw);
|
|
2046
|
+
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return { error: `WATCH_INTERVAL "${raw}" is out of range` };
|
|
2047
|
+
return { value: parsed };
|
|
2048
|
+
};
|
|
2049
|
+
/**
|
|
2050
|
+
* Spawn the `\watch` pager for the full duration of the polling loop.
|
|
2051
|
+
*
|
|
2052
|
+
* Upstream `do_watch` wraps the loop in a single `popen` of
|
|
2053
|
+
* `PSQL_WATCH_PAGER`. It deliberately ignores `PSQL_PAGER` and `$PAGER`:
|
|
2054
|
+
*
|
|
2055
|
+
* > we ignore the regular PSQL_PAGER or PAGER environment variables,
|
|
2056
|
+
* > because traditional pagers probably won't be very useful for
|
|
2057
|
+
* > showing a stream of results.
|
|
2058
|
+
*
|
|
2059
|
+
* Mirror that here. Reading from `$PAGER` would silently hijack
|
|
2060
|
+
* `\watch` output for any user whose shell sets `PAGER=less` (the
|
|
2061
|
+
* common default), which makes the loop's output disappear into a
|
|
2062
|
+
* subprocess that diff harnesses can't capture.
|
|
2063
|
+
*
|
|
2064
|
+
* We use a single `sh -c <pager>` spawn so the user can set the
|
|
2065
|
+
* variable to a full command string (`less -R`, `tee /tmp/log`, …)
|
|
2066
|
+
* without the caller having to tokenise it. EPIPE on the stdin pipe is
|
|
2067
|
+
* swallowed for the same reason as in `openWriter`: the user may quit
|
|
2068
|
+
* `less` while we still have writes pending in the next iteration.
|
|
2069
|
+
*
|
|
2070
|
+
* Returns `null` when `PSQL_WATCH_PAGER` is unset or whitespace-only
|
|
2071
|
+
* (upstream's "no pager" rule), so the caller falls back to the normal
|
|
2072
|
+
* output target.
|
|
2073
|
+
*/
|
|
2074
|
+
const openWatchPager = () => {
|
|
2075
|
+
const cmd = process.env.PSQL_WATCH_PAGER ?? "";
|
|
2076
|
+
if (cmd.trim().length === 0) return null;
|
|
2077
|
+
const child = spawn("sh", ["-c", cmd], { stdio: [
|
|
2078
|
+
"pipe",
|
|
2079
|
+
"inherit",
|
|
2080
|
+
"inherit"
|
|
2081
|
+
] });
|
|
2082
|
+
child.stdin.on("error", (err) => {
|
|
2083
|
+
if (err.code !== "EPIPE") throw err;
|
|
2084
|
+
});
|
|
2085
|
+
return {
|
|
2086
|
+
stream: child.stdin,
|
|
2087
|
+
close: () => new Promise((resolve) => {
|
|
2088
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
2089
|
+
resolve();
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
let settled = false;
|
|
2093
|
+
const finish = () => {
|
|
2094
|
+
if (settled) return;
|
|
2095
|
+
settled = true;
|
|
2096
|
+
resolve();
|
|
2097
|
+
};
|
|
2098
|
+
child.once("close", finish);
|
|
2099
|
+
child.once("error", finish);
|
|
2100
|
+
if (!child.stdin.destroyed) try {
|
|
2101
|
+
child.stdin.end();
|
|
2102
|
+
} catch (err) {
|
|
2103
|
+
if (err.code !== "EPIPE") finish();
|
|
2104
|
+
}
|
|
2105
|
+
})
|
|
2106
|
+
};
|
|
2107
|
+
};
|
|
2108
|
+
const cmdWatch = {
|
|
2109
|
+
name: "watch",
|
|
2110
|
+
helpKey: "watch",
|
|
2111
|
+
async run(ctx) {
|
|
2112
|
+
const gated = pipelineGate(ctx);
|
|
2113
|
+
if (gated !== null) return gated;
|
|
2114
|
+
const trimmedBuf = stripLeadingCommentsAndWS(ctx.queryBuf);
|
|
2115
|
+
const sql = trimmedBuf.trim();
|
|
2116
|
+
if (sql.length === 0) return errResult$1(ctx, "no query buffer");
|
|
2117
|
+
if (!ctx.settings.db) return errResult$1(ctx, "no connection to the server");
|
|
2118
|
+
let intervalSet = false;
|
|
2119
|
+
let interval = null;
|
|
2120
|
+
let iterSet = false;
|
|
2121
|
+
let iterMax = 0;
|
|
2122
|
+
let minRowsSet = false;
|
|
2123
|
+
let minRows = 0;
|
|
2124
|
+
let positionalSeen = false;
|
|
2125
|
+
while (true) {
|
|
2126
|
+
const arg = ctx.nextArg("normal");
|
|
2127
|
+
if (arg === null) break;
|
|
2128
|
+
if (arg.length === 0) continue;
|
|
2129
|
+
const eqIdx = arg.indexOf("=");
|
|
2130
|
+
if (eqIdx > 0) {
|
|
2131
|
+
const key = arg.slice(0, eqIdx);
|
|
2132
|
+
const value = arg.slice(eqIdx + 1);
|
|
2133
|
+
if (key === "i") {
|
|
2134
|
+
if (intervalSet) return errResult$1(ctx, "interval value is specified more than once");
|
|
2135
|
+
const parsed = parseStrictNonNegativeFloat(value);
|
|
2136
|
+
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return errResult$1(ctx, `incorrect interval value "${value}"`);
|
|
2137
|
+
interval = parsed;
|
|
2138
|
+
intervalSet = true;
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
if (key === "c") {
|
|
2142
|
+
if (iterSet) return errResult$1(ctx, "iteration count is specified more than once");
|
|
2143
|
+
const parsed = parseStrictNonNegativeInt(value);
|
|
2144
|
+
if (parsed === null || parsed === 0) return errResult$1(ctx, `incorrect iteration count "${value}"`);
|
|
2145
|
+
iterMax = parsed;
|
|
2146
|
+
iterSet = true;
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
if (key === "m" || key === "min_rows") {
|
|
2150
|
+
if (minRowsSet) return errResult$1(ctx, "minimum row count specified more than once");
|
|
2151
|
+
const parsed = parseStrictNonNegativeInt(value);
|
|
2152
|
+
if (parsed === null) return errResult$1(ctx, `incorrect minimum row count "${value}"`);
|
|
2153
|
+
minRows = parsed;
|
|
2154
|
+
minRowsSet = true;
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
return errResult$1(ctx, `unrecognized option "${key}"`);
|
|
2158
|
+
}
|
|
2159
|
+
if (positionalSeen || intervalSet) return errResult$1(ctx, "interval value is specified more than once");
|
|
2160
|
+
const parsed = parseStrictNonNegativeFloat(arg);
|
|
2161
|
+
if (parsed === null || parsed > WATCH_INTERVAL_MAX_SECONDS) return errResult$1(ctx, `incorrect interval value "${arg}"`);
|
|
2162
|
+
interval = parsed;
|
|
2163
|
+
intervalSet = true;
|
|
2164
|
+
positionalSeen = true;
|
|
2165
|
+
}
|
|
2166
|
+
if (interval === null) {
|
|
2167
|
+
const resolved = resolveWatchIntervalDefault(ctx.settings);
|
|
2168
|
+
if ("error" in resolved) return errResult$1(ctx, resolved.error);
|
|
2169
|
+
interval = resolved.value;
|
|
2170
|
+
}
|
|
2171
|
+
const intervalMs = Math.round(interval * 1e3);
|
|
2172
|
+
const controller = WATCH_TEST_CONTROLLER.ref ?? new AbortController();
|
|
2173
|
+
const sigintHandler = () => {
|
|
2174
|
+
controller.abort();
|
|
2175
|
+
};
|
|
2176
|
+
const installedSigint = WATCH_TEST_CONTROLLER.ref === null;
|
|
2177
|
+
if (installedSigint) process.once("SIGINT", sigintHandler);
|
|
2178
|
+
const pager = openWatchPager();
|
|
2179
|
+
const out = pager?.stream ?? pickOut(ctx.settings, null);
|
|
2180
|
+
try {
|
|
2181
|
+
let iter = 0;
|
|
2182
|
+
while (!controller.signal.aborted) {
|
|
2183
|
+
iter++;
|
|
2184
|
+
const stamp = formatWatchTimestamp(/* @__PURE__ */ new Date());
|
|
2185
|
+
out.write(`${stamp} (every ${String(interval)}s)\n\n`);
|
|
2186
|
+
let lastRowCount = 0;
|
|
2187
|
+
try {
|
|
2188
|
+
const results = await ctx.settings.db.execSimple(sql);
|
|
2189
|
+
for (const rs of results) if (rs.fields.length > 0) {
|
|
2190
|
+
await renderResult(ctx.settings, rs, out);
|
|
2191
|
+
lastRowCount = rs.rows.length;
|
|
2192
|
+
}
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
return formatServerError(ctx, err, trimmedBuf);
|
|
2195
|
+
}
|
|
2196
|
+
if (iterSet && iter >= iterMax) break;
|
|
2197
|
+
if (minRowsSet && lastRowCount < minRows) break;
|
|
2198
|
+
if (controller.signal.aborted) break;
|
|
2199
|
+
if (intervalMs > 0) await sleepCancellable(intervalMs, controller.signal);
|
|
2200
|
+
}
|
|
2201
|
+
if (!pager) out.write("\n");
|
|
2202
|
+
return {
|
|
2203
|
+
status: "reset-buf",
|
|
2204
|
+
newBuf: ""
|
|
2205
|
+
};
|
|
2206
|
+
} finally {
|
|
2207
|
+
if (installedSigint) process.removeListener("SIGINT", sigintHandler);
|
|
2208
|
+
if (pager) await pager.close();
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
const registerIoCommands = (registry) => {
|
|
2213
|
+
registry.register(cmdInclude);
|
|
2214
|
+
registry.register(cmdIncludeRel);
|
|
2215
|
+
registry.register(cmdOut);
|
|
2216
|
+
registry.register(cmdWrite);
|
|
2217
|
+
registry.register(cmdG);
|
|
2218
|
+
registry.register(cmdGx);
|
|
2219
|
+
registry.register(cmdPrint);
|
|
2220
|
+
registry.register(cmdGset);
|
|
2221
|
+
registry.register(cmdGdesc);
|
|
2222
|
+
registry.register(cmdGexec);
|
|
2223
|
+
registry.register(cmdWatch);
|
|
2224
|
+
};
|
|
2225
|
+
//#endregion
|
|
2226
|
+
//#region src/psql/command/cmd_pipeline.ts
|
|
2227
|
+
/**
|
|
2228
|
+
* psql pipeline / extended-query backslash commands (WP-21).
|
|
2229
|
+
*
|
|
2230
|
+
* Implements the subset of upstream psql's "pipeline mode" backslash commands
|
|
2231
|
+
* that drive the extended protocol directly. These are typically used together
|
|
2232
|
+
* with the existing `\g` flow: a `\bind` (or `\parse`) command stashes
|
|
2233
|
+
* parameter / statement state on the {@link BackslashContext}'s settings via
|
|
2234
|
+
* a Symbol-keyed slot; when the mainloop then sees a `;` (or a `\g`), it can
|
|
2235
|
+
* notice the stashed state and route the query through
|
|
2236
|
+
* `Connection.query(sql, params)` instead of `execSimple`.
|
|
2237
|
+
*
|
|
2238
|
+
* Because the mainloop integration is owned by other WPs (and was deliberately
|
|
2239
|
+
* left untouched here), this module exposes both the command specs AND a
|
|
2240
|
+
* small helper, {@link getPipelineState}, that the mainloop will use to
|
|
2241
|
+
* consult the stashed state. The commands operate on the buffered query the
|
|
2242
|
+
* same way `\g` does — they execute or queue the buffer and reset it.
|
|
2243
|
+
*
|
|
2244
|
+
* Commands shipped:
|
|
2245
|
+
*
|
|
2246
|
+
* \bind [VALUE ...] stash params for next ; / \g
|
|
2247
|
+
* \bind_named NAME [V ...] stash params + named prepared statement
|
|
2248
|
+
* \parse NAME prepare current query buffer as NAME
|
|
2249
|
+
* \close_prepared NAME Close('S', NAME)
|
|
2250
|
+
* \startpipeline begin a pipeline session (settings.sendMode)
|
|
2251
|
+
* \endpipeline end the pipeline session, drain results
|
|
2252
|
+
* \syncpipeline send Sync mid-pipeline
|
|
2253
|
+
* \sendpipeline submit the current buffered query w/o waiting
|
|
2254
|
+
* \flushrequest send Flush
|
|
2255
|
+
* \flush alias for \flushrequest
|
|
2256
|
+
* \getresults [N] drain pending pipeline results
|
|
2257
|
+
* \gdesc describe-without-execute the buffered query
|
|
2258
|
+
*
|
|
2259
|
+
* The set is registered in bulk via {@link registerPipelineCommands}.
|
|
2260
|
+
*/
|
|
2261
|
+
const BIND_STATE_KEY = Symbol.for("neonctl.psql.bindState");
|
|
2262
|
+
const PIPELINE_KEY = Symbol.for("neonctl.psql.pipeline");
|
|
2263
|
+
const PREPARED_BY_NAME_KEY = Symbol.for("neonctl.psql.preparedByName");
|
|
2264
|
+
const stashOf = (settings) => settings;
|
|
2265
|
+
/** Read (and clear) the pending bind params, if any. */
|
|
2266
|
+
const consumeBindState = (settings) => {
|
|
2267
|
+
const s = stashOf(settings);
|
|
2268
|
+
const cur = s[BIND_STATE_KEY] ?? null;
|
|
2269
|
+
s[BIND_STATE_KEY] = void 0;
|
|
2270
|
+
return cur;
|
|
2271
|
+
};
|
|
2272
|
+
/**
|
|
2273
|
+
* Peek at the bind stash without consuming. Used by `\g` to decide
|
|
2274
|
+
* whether to skip the "empty buffer, no prior query" no-op guard:
|
|
2275
|
+
* when a `\bind_named NAME` is pending, the prepared statement carries
|
|
2276
|
+
* the SQL server-side so no buffer text is needed.
|
|
2277
|
+
*/
|
|
2278
|
+
const stagedNamedBindPresent = (settings) => {
|
|
2279
|
+
const cur = stashOf(settings)[BIND_STATE_KEY];
|
|
2280
|
+
return !!cur && cur.byName;
|
|
2281
|
+
};
|
|
2282
|
+
/** Stash a PreparedStatement for later `\bind_named NAME \g` lookup. */
|
|
2283
|
+
const stashPrepared = (settings, name, ps) => {
|
|
2284
|
+
const s = stashOf(settings);
|
|
2285
|
+
let map = s[PREPARED_BY_NAME_KEY];
|
|
2286
|
+
if (!map) {
|
|
2287
|
+
map = /* @__PURE__ */ new Map();
|
|
2288
|
+
s[PREPARED_BY_NAME_KEY] = map;
|
|
2289
|
+
}
|
|
2290
|
+
map.set(name, ps);
|
|
2291
|
+
};
|
|
2292
|
+
/** Look up a previously-stashed PreparedStatement by name. */
|
|
2293
|
+
const lookupPrepared = (settings, name) => {
|
|
2294
|
+
return stashOf(settings)[PREPARED_BY_NAME_KEY]?.get(name) ?? null;
|
|
2295
|
+
};
|
|
2296
|
+
/** Drop a stashed PreparedStatement (after `\close_prepared`). */
|
|
2297
|
+
const dropPrepared = (settings, name) => {
|
|
2298
|
+
const map = stashOf(settings)[PREPARED_BY_NAME_KEY];
|
|
2299
|
+
if (map) map.delete(name);
|
|
2300
|
+
};
|
|
2301
|
+
/** Peek at the current pipeline session (or null). */
|
|
2302
|
+
const getPipelineState = (settings) => stashOf(settings)[PIPELINE_KEY] ?? null;
|
|
2303
|
+
const readCounter = (settings, name) => {
|
|
2304
|
+
const raw = settings.vars.get(name);
|
|
2305
|
+
if (raw === void 0) return 0;
|
|
2306
|
+
const n = parseInt(raw, 10);
|
|
2307
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
2308
|
+
};
|
|
2309
|
+
const setCounter = (settings, name, value) => {
|
|
2310
|
+
settings.vars.set(name, String(Math.max(0, value)));
|
|
2311
|
+
};
|
|
2312
|
+
const bumpCounter = (settings, name, delta) => {
|
|
2313
|
+
setCounter(settings, name, readCounter(settings, name) + delta);
|
|
2314
|
+
};
|
|
2315
|
+
const resetPipelineCounters = (settings) => {
|
|
2316
|
+
setCounter(settings, "PIPELINE_COMMAND_COUNT", 0);
|
|
2317
|
+
setCounter(settings, "PIPELINE_SYNC_COUNT", 0);
|
|
2318
|
+
setCounter(settings, "PIPELINE_RESULT_COUNT", 0);
|
|
2319
|
+
};
|
|
2320
|
+
/**
|
|
2321
|
+
* Wrap `Pipeline.execute` so each enqueued Execute message bumps
|
|
2322
|
+
* `PIPELINE_COMMAND_COUNT`. The wrapping covers both call sites:
|
|
2323
|
+
*
|
|
2324
|
+
* - `cmdSendPipeline` (this file) — `\sendpipeline`.
|
|
2325
|
+
* - `dispatchSendQuery` (mainloop) — implicit `;`-queries while pipeline
|
|
2326
|
+
* is active. Mainloop calls `ps.session.execute('', 0)` so the wrapper
|
|
2327
|
+
* fires automatically without mainloop knowing about the var store.
|
|
2328
|
+
*
|
|
2329
|
+
* The wrapper preserves the original function's `this` binding via `apply`.
|
|
2330
|
+
*/
|
|
2331
|
+
const wrapSessionForCounters = (session, settings) => {
|
|
2332
|
+
const origExecute = session.execute.bind(session);
|
|
2333
|
+
session.execute = (name, maxRows) => {
|
|
2334
|
+
bumpCounter(settings, "PIPELINE_COMMAND_COUNT", 1);
|
|
2335
|
+
return origExecute(name, maxRows);
|
|
2336
|
+
};
|
|
2337
|
+
return session;
|
|
2338
|
+
};
|
|
2339
|
+
const errResult = (ctx, message) => {
|
|
2340
|
+
ctx.settings.lastErrorResult = { message };
|
|
2341
|
+
writeErr(`\\${ctx.cmdName}: ${message}\n`);
|
|
2342
|
+
return {
|
|
2343
|
+
status: "error",
|
|
2344
|
+
errorWritten: true
|
|
2345
|
+
};
|
|
2346
|
+
};
|
|
2347
|
+
/**
|
|
2348
|
+
* Coerce any thrown / rejected value into a printable string. The
|
|
2349
|
+
* extended-protocol driver in `PgConnection` rejects with raw
|
|
2350
|
+
* ConnectError records (`{severity, code, message, detail, ...}`) —
|
|
2351
|
+
* not `Error` instances — so `String(err)` would produce
|
|
2352
|
+
* `[object Object]` in the conformance output. We probe for a
|
|
2353
|
+
* `.message` property (covering both `Error` and the ConnectError
|
|
2354
|
+
* shape) and fall back to JSON.stringify only when there's no
|
|
2355
|
+
* message field at all.
|
|
2356
|
+
*/
|
|
2357
|
+
const errorToMessage = (err) => {
|
|
2358
|
+
if (err instanceof Error) return err.message;
|
|
2359
|
+
if (err !== null && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message;
|
|
2360
|
+
try {
|
|
2361
|
+
return JSON.stringify(err);
|
|
2362
|
+
} catch {
|
|
2363
|
+
return String(err);
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
/**
|
|
2367
|
+
* Render a pipeline-collected ConnectError to stderr in the upstream
|
|
2368
|
+
* shape: a `SEVERITY: message` line, followed by optional `DETAIL:` /
|
|
2369
|
+
* `HINT:` / `CONTEXT:` lines (each on its own line). Matches the
|
|
2370
|
+
* subset of `formatErrorReport`-style output the conformance corpus
|
|
2371
|
+
* expects from `\endpipeline` for non-FATAL pipeline errors. We
|
|
2372
|
+
* inline a minimal renderer here rather than importing the full
|
|
2373
|
+
* `formatErrorReport` from `cmd_meta` because we always want the
|
|
2374
|
+
* "default verbosity, no LINE/caret context" form — pipeline errors
|
|
2375
|
+
* arrive after the buffered query has been reset and we don't carry
|
|
2376
|
+
* the originating SQL position through `session.lastError`.
|
|
2377
|
+
*/
|
|
2378
|
+
const renderPipelineError = (err) => {
|
|
2379
|
+
if (err === null || typeof err !== "object") {
|
|
2380
|
+
writeErr(`ERROR: ${errorToMessage(err)}\n`);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const e = err;
|
|
2384
|
+
if (e.pipelineAborted) {
|
|
2385
|
+
writeErr(`${e.message ?? "Pipeline aborted, command did not run"}\n`);
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
const severity = e.severity ?? "ERROR";
|
|
2389
|
+
const message = e.message ?? "";
|
|
2390
|
+
writeErr(`${severity}: ${message}\n`);
|
|
2391
|
+
if (e.detail) writeErr(`DETAIL: ${e.detail}\n`);
|
|
2392
|
+
if (e.hint) writeErr(`HINT: ${e.hint}\n`);
|
|
2393
|
+
if (e.where) writeErr(`CONTEXT: ${e.where}\n`);
|
|
2394
|
+
};
|
|
2395
|
+
const readAllArgs = (ctx) => {
|
|
2396
|
+
const out = [];
|
|
2397
|
+
for (;;) {
|
|
2398
|
+
const arg = ctx.nextArg("normal");
|
|
2399
|
+
if (arg === null) break;
|
|
2400
|
+
out.push(arg);
|
|
2401
|
+
}
|
|
2402
|
+
return out;
|
|
2403
|
+
};
|
|
2404
|
+
const cmdBind = {
|
|
2405
|
+
name: "bind",
|
|
2406
|
+
helpKey: "bind",
|
|
2407
|
+
run(ctx) {
|
|
2408
|
+
const values = readAllArgs(ctx);
|
|
2409
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = {
|
|
2410
|
+
name: "",
|
|
2411
|
+
values,
|
|
2412
|
+
byName: false
|
|
2413
|
+
};
|
|
2414
|
+
return Promise.resolve({ status: "ok" });
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
const cmdBindNamed = {
|
|
2418
|
+
name: "bind_named",
|
|
2419
|
+
helpKey: "bind_named",
|
|
2420
|
+
run(ctx) {
|
|
2421
|
+
const name = ctx.nextArg("normal");
|
|
2422
|
+
if (name === null) {
|
|
2423
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = void 0;
|
|
2424
|
+
return Promise.resolve(errResult(ctx, "missing required argument"));
|
|
2425
|
+
}
|
|
2426
|
+
const values = readAllArgs(ctx);
|
|
2427
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = {
|
|
2428
|
+
name,
|
|
2429
|
+
values,
|
|
2430
|
+
byName: true
|
|
2431
|
+
};
|
|
2432
|
+
return Promise.resolve({ status: "ok" });
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
const cmdParse = {
|
|
2436
|
+
name: "parse",
|
|
2437
|
+
helpKey: "parse",
|
|
2438
|
+
async run(ctx) {
|
|
2439
|
+
const name = ctx.nextArg("normal");
|
|
2440
|
+
if (name === null) return errResult(ctx, "missing required argument");
|
|
2441
|
+
const sql = ctx.queryBuf;
|
|
2442
|
+
if (sql.trim().length === 0) return errResult(ctx, "no query buffer");
|
|
2443
|
+
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
2444
|
+
const pipelineActive = getPipelineState(ctx.settings);
|
|
2445
|
+
if (pipelineActive !== null) try {
|
|
2446
|
+
const session = pipelineActive.session;
|
|
2447
|
+
if (typeof session.parseSlot === "function") await session.parseSlot(name, sql, []);
|
|
2448
|
+
else await session.parse(name, sql, []);
|
|
2449
|
+
ctx.settings.lastQuery = sql;
|
|
2450
|
+
bumpCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 1);
|
|
2451
|
+
return {
|
|
2452
|
+
status: "reset-buf",
|
|
2453
|
+
newBuf: ""
|
|
2454
|
+
};
|
|
2455
|
+
} catch (err) {
|
|
2456
|
+
return errResult(ctx, errorToMessage(err));
|
|
2457
|
+
}
|
|
2458
|
+
try {
|
|
2459
|
+
const ps = await ctx.settings.db.prepare(name, sql);
|
|
2460
|
+
stashPrepared(ctx.settings, name, ps);
|
|
2461
|
+
ctx.settings.lastQuery = sql;
|
|
2462
|
+
return {
|
|
2463
|
+
status: "reset-buf",
|
|
2464
|
+
newBuf: ""
|
|
2465
|
+
};
|
|
2466
|
+
} catch (err) {
|
|
2467
|
+
return errResult(ctx, errorToMessage(err));
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
const cmdClosePrepared = {
|
|
2472
|
+
name: "close_prepared",
|
|
2473
|
+
helpKey: "close_prepared",
|
|
2474
|
+
async run(ctx) {
|
|
2475
|
+
const name = ctx.nextArg("normal");
|
|
2476
|
+
if (name === null) return errResult(ctx, "missing required argument");
|
|
2477
|
+
const db = ctx.settings.db;
|
|
2478
|
+
if (!db) return errResult(ctx, "no connection to the server");
|
|
2479
|
+
const ps = getPipelineState(ctx.settings);
|
|
2480
|
+
if (ps !== null) try {
|
|
2481
|
+
const session = ps.session;
|
|
2482
|
+
if (typeof session.closeSlot === "function") await session.closeSlot(name);
|
|
2483
|
+
else await session.close(name);
|
|
2484
|
+
bumpCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 1);
|
|
2485
|
+
dropPrepared(ctx.settings, name);
|
|
2486
|
+
return { status: "ok" };
|
|
2487
|
+
} catch (err) {
|
|
2488
|
+
return errResult(ctx, errorToMessage(err));
|
|
2489
|
+
}
|
|
2490
|
+
try {
|
|
2491
|
+
if (db.closePreparedStatement) await db.closePreparedStatement(name);
|
|
2492
|
+
else await (await db.prepare(name, "SELECT 1")).close();
|
|
2493
|
+
dropPrepared(ctx.settings, name);
|
|
2494
|
+
return { status: "ok" };
|
|
2495
|
+
} catch (err) {
|
|
2496
|
+
return errResult(ctx, errorToMessage(err));
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
const cmdStartPipeline = {
|
|
2501
|
+
name: "startpipeline",
|
|
2502
|
+
helpKey: "startpipeline",
|
|
2503
|
+
run(ctx) {
|
|
2504
|
+
if (!ctx.settings.db) return Promise.resolve(errResult(ctx, "no connection to the server"));
|
|
2505
|
+
if (getPipelineState(ctx.settings) !== null) return Promise.resolve({ status: "ok" });
|
|
2506
|
+
try {
|
|
2507
|
+
const session = wrapSessionForCounters(ctx.settings.db.pipeline(), ctx.settings);
|
|
2508
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = {
|
|
2509
|
+
session,
|
|
2510
|
+
pending: [],
|
|
2511
|
+
drainedCount: 0
|
|
2512
|
+
};
|
|
2513
|
+
ctx.settings.sendMode = "extended-pipeline";
|
|
2514
|
+
resetPipelineCounters(ctx.settings);
|
|
2515
|
+
return Promise.resolve({ status: "ok" });
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
return Promise.resolve(errResult(ctx, errorToMessage(err)));
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2521
|
+
const cmdEndPipeline = {
|
|
2522
|
+
name: "endpipeline",
|
|
2523
|
+
helpKey: "endpipeline",
|
|
2524
|
+
async run(ctx) {
|
|
2525
|
+
const ps = getPipelineState(ctx.settings);
|
|
2526
|
+
if (!ps) {
|
|
2527
|
+
ctx.settings.lastErrorResult = { message: "cannot send pipeline when not in pipeline mode" };
|
|
2528
|
+
writeErr("cannot send pipeline when not in pipeline mode\n");
|
|
2529
|
+
return {
|
|
2530
|
+
status: "error",
|
|
2531
|
+
errorWritten: true
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
try {
|
|
2535
|
+
const alreadyDrained = ps.drainedCount;
|
|
2536
|
+
const session = ps.session;
|
|
2537
|
+
const cmdSlotsSnapshot = Array.isArray(session.cmdSlots) ? session.cmdSlots.slice() : [];
|
|
2538
|
+
const fetchCountActive = (ctx.settings.vars.get("FETCH_COUNT") ?? "0") !== "0";
|
|
2539
|
+
const sets = await ps.session.end();
|
|
2540
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = void 0;
|
|
2541
|
+
ctx.settings.sendMode = "extended-query";
|
|
2542
|
+
resetPipelineCounters(ctx.settings);
|
|
2543
|
+
clearPipelineGateErrors(ctx.settings);
|
|
2544
|
+
const settled = await Promise.allSettled(cmdSlotsSnapshot);
|
|
2545
|
+
let errorRendered = false;
|
|
2546
|
+
const entries = settled.length > 0 ? settled : sets.map((rs) => ({
|
|
2547
|
+
status: "fulfilled",
|
|
2548
|
+
value: rs
|
|
2549
|
+
}));
|
|
2550
|
+
const sliceForError = entries.slice(alreadyDrained);
|
|
2551
|
+
const realLastError = (() => {
|
|
2552
|
+
for (const r of sliceForError) {
|
|
2553
|
+
if (r.status !== "rejected") continue;
|
|
2554
|
+
const reason = r.reason;
|
|
2555
|
+
if (!(typeof reason === "object" && reason !== null && reason.pipelineAborted === true)) return reason;
|
|
2556
|
+
}
|
|
2557
|
+
return null;
|
|
2558
|
+
})() ?? (() => {
|
|
2559
|
+
const le = session.lastError;
|
|
2560
|
+
if (le === null || le === void 0) return null;
|
|
2561
|
+
if (typeof le === "object" && le.pipelineAborted) return null;
|
|
2562
|
+
return le;
|
|
2563
|
+
})();
|
|
2564
|
+
for (let i = alreadyDrained; i < entries.length; i++) {
|
|
2565
|
+
const r = entries[i];
|
|
2566
|
+
if (r.status === "fulfilled") {
|
|
2567
|
+
const rs = r.value;
|
|
2568
|
+
for (const n of rs.notices) {
|
|
2569
|
+
let out = `${n.severity}: ${n.message}\n`;
|
|
2570
|
+
if (n.detail !== void 0) out += `DETAIL: ${n.detail}\n`;
|
|
2571
|
+
if (n.hint !== void 0) out += `HINT: ${n.hint}\n`;
|
|
2572
|
+
writeErr(out);
|
|
2573
|
+
}
|
|
2574
|
+
const isSyncOrPlaceholder = rs.fields.length === 0 && rs.command === "" && rs.rows.length === 0;
|
|
2575
|
+
const isCommandOnly = rs.fields.length === 0 && rs.rows.length === 0 && rs.command !== "";
|
|
2576
|
+
if (!isSyncOrPlaceholder && !isCommandOnly) if (rs.fields.length === 0 && rs.rows.length > 0) {
|
|
2577
|
+
if (!ctx.settings.popt.topt.tuplesOnly) {
|
|
2578
|
+
process.stdout.write("--\n");
|
|
2579
|
+
process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
|
|
2580
|
+
}
|
|
2581
|
+
} else await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
|
|
2582
|
+
} else if (!errorRendered) {
|
|
2583
|
+
const reason = r.reason;
|
|
2584
|
+
const isAborted = typeof reason === "object" && reason !== null && reason.pipelineAborted === true;
|
|
2585
|
+
if (fetchCountActive) writeErr("fetching results in chunked mode failed\n");
|
|
2586
|
+
if (isAborted && realLastError !== null) renderPipelineError(realLastError);
|
|
2587
|
+
else if (isAborted) writeErr("Pipeline aborted, command did not run\n");
|
|
2588
|
+
else renderPipelineError(reason);
|
|
2589
|
+
errorRendered = true;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (!errorRendered) {
|
|
2593
|
+
const lastErr = session.lastError;
|
|
2594
|
+
if (lastErr !== null && lastErr !== void 0) {
|
|
2595
|
+
if (fetchCountActive) writeErr("fetching results in chunked mode failed\n");
|
|
2596
|
+
renderPipelineError(lastErr);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
return { status: "ok" };
|
|
2600
|
+
} catch (err) {
|
|
2601
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = void 0;
|
|
2602
|
+
ctx.settings.sendMode = "extended-query";
|
|
2603
|
+
resetPipelineCounters(ctx.settings);
|
|
2604
|
+
clearPipelineGateErrors(ctx.settings);
|
|
2605
|
+
return errResult(ctx, errorToMessage(err));
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
const cmdSyncPipeline = {
|
|
2610
|
+
name: "syncpipeline",
|
|
2611
|
+
helpKey: "syncpipeline",
|
|
2612
|
+
async run(ctx) {
|
|
2613
|
+
const ps = getPipelineState(ctx.settings);
|
|
2614
|
+
if (!ps) return errResult(ctx, "no pipeline active");
|
|
2615
|
+
try {
|
|
2616
|
+
await ps.session.sync();
|
|
2617
|
+
const queued = readCounter(ctx.settings, "PIPELINE_COMMAND_COUNT");
|
|
2618
|
+
setCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 0);
|
|
2619
|
+
bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", queued);
|
|
2620
|
+
bumpCounter(ctx.settings, "PIPELINE_SYNC_COUNT", 1);
|
|
2621
|
+
return { status: "ok" };
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
return errResult(ctx, errorToMessage(err));
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
const cmdFlushRequest = {
|
|
2628
|
+
name: "flushrequest",
|
|
2629
|
+
helpKey: "flushrequest",
|
|
2630
|
+
async run(ctx) {
|
|
2631
|
+
const ps = getPipelineState(ctx.settings);
|
|
2632
|
+
if (!ps) return errResult(ctx, "no pipeline active");
|
|
2633
|
+
try {
|
|
2634
|
+
await ps.session.flush();
|
|
2635
|
+
const queued = readCounter(ctx.settings, "PIPELINE_COMMAND_COUNT");
|
|
2636
|
+
setCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 0);
|
|
2637
|
+
bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", queued);
|
|
2638
|
+
return { status: "ok" };
|
|
2639
|
+
} catch (err) {
|
|
2640
|
+
return errResult(ctx, errorToMessage(err));
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
const cmdFlush = {
|
|
2645
|
+
name: "flush",
|
|
2646
|
+
helpKey: "flush",
|
|
2647
|
+
run(ctx) {
|
|
2648
|
+
return cmdFlushRequest.run(ctx);
|
|
2649
|
+
}
|
|
2650
|
+
};
|
|
2651
|
+
const cmdSendPipeline = {
|
|
2652
|
+
name: "sendpipeline",
|
|
2653
|
+
helpKey: "sendpipeline",
|
|
2654
|
+
async run(ctx) {
|
|
2655
|
+
const ps = getPipelineState(ctx.settings);
|
|
2656
|
+
if (!ps) {
|
|
2657
|
+
consumeBindState(ctx.settings);
|
|
2658
|
+
ctx.settings.lastErrorResult = { message: "\\sendpipeline not allowed outside of pipeline mode" };
|
|
2659
|
+
writeErr("\\sendpipeline not allowed outside of pipeline mode\n");
|
|
2660
|
+
return {
|
|
2661
|
+
status: "error",
|
|
2662
|
+
errorWritten: true
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
const bind = consumeBindState(ctx.settings);
|
|
2666
|
+
if (bind === null) {
|
|
2667
|
+
ctx.settings.lastErrorResult = { message: "\\sendpipeline must be used after \\bind or \\bind_named" };
|
|
2668
|
+
writeErr("\\sendpipeline must be used after \\bind or \\bind_named\n");
|
|
2669
|
+
return {
|
|
2670
|
+
status: "error",
|
|
2671
|
+
errorWritten: true
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
const sql = ctx.queryBuf.trim();
|
|
2675
|
+
const stmtName = bind.name;
|
|
2676
|
+
const params = bind.values;
|
|
2677
|
+
if (!bind.byName && sql.length === 0) return errResult(ctx, "no query buffer");
|
|
2678
|
+
try {
|
|
2679
|
+
if (!bind.byName) await ps.session.parse("", sql, []);
|
|
2680
|
+
await ps.session.bind(stmtName, params);
|
|
2681
|
+
const exec = (async () => {
|
|
2682
|
+
await ps.session.execute("", 0);
|
|
2683
|
+
return {
|
|
2684
|
+
command: "",
|
|
2685
|
+
rowCount: null,
|
|
2686
|
+
oid: null,
|
|
2687
|
+
fields: [],
|
|
2688
|
+
rows: [],
|
|
2689
|
+
notices: []
|
|
2690
|
+
};
|
|
2691
|
+
})();
|
|
2692
|
+
ps.pending.push(exec);
|
|
2693
|
+
return {
|
|
2694
|
+
status: "reset-buf",
|
|
2695
|
+
newBuf: ""
|
|
2696
|
+
};
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
return errResult(ctx, errorToMessage(err));
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
};
|
|
2702
|
+
const cmdGetResults = {
|
|
2703
|
+
name: "getresults",
|
|
2704
|
+
helpKey: "getresults",
|
|
2705
|
+
async run(ctx) {
|
|
2706
|
+
const ps = getPipelineState(ctx.settings);
|
|
2707
|
+
const arg = ctx.nextArg("normal");
|
|
2708
|
+
let requested = null;
|
|
2709
|
+
if (arg !== null && arg.length > 0) {
|
|
2710
|
+
const parsed = parseInt(arg, 10);
|
|
2711
|
+
if (!Number.isFinite(parsed) || parsed < 0) return errResult(ctx, "invalid number of requested results");
|
|
2712
|
+
requested = parsed;
|
|
2713
|
+
}
|
|
2714
|
+
if (!ps) {
|
|
2715
|
+
process.stdout.write("No pending results to get\n");
|
|
2716
|
+
return { status: "ok" };
|
|
2717
|
+
}
|
|
2718
|
+
const syncAvailable = readCounter(ctx.settings, "PIPELINE_SYNC_COUNT");
|
|
2719
|
+
const resultAvailable = readCounter(ctx.settings, "PIPELINE_RESULT_COUNT");
|
|
2720
|
+
const available = syncAvailable + resultAvailable;
|
|
2721
|
+
if (available === 0) {
|
|
2722
|
+
process.stdout.write("No pending results to get\n");
|
|
2723
|
+
return { status: "ok" };
|
|
2724
|
+
}
|
|
2725
|
+
const n = requested === null || requested === 0 ? available : Math.min(requested, available);
|
|
2726
|
+
const session = ps.session;
|
|
2727
|
+
const slots = Array.isArray(session.cmdSlots) ? session.cmdSlots : [];
|
|
2728
|
+
const start = ps.drainedCount;
|
|
2729
|
+
const end = start + n;
|
|
2730
|
+
const slice = slots.slice(start, end);
|
|
2731
|
+
ps.drainedCount = end;
|
|
2732
|
+
ps.pending.splice(0, n);
|
|
2733
|
+
try {
|
|
2734
|
+
const settled = await Promise.allSettled(slice);
|
|
2735
|
+
const sliceErr = (() => {
|
|
2736
|
+
for (const r of settled) {
|
|
2737
|
+
if (r.status !== "rejected") continue;
|
|
2738
|
+
const reason = r.reason;
|
|
2739
|
+
if (!(typeof reason === "object" && reason !== null && reason.pipelineAborted === true)) return reason;
|
|
2740
|
+
}
|
|
2741
|
+
return null;
|
|
2742
|
+
})();
|
|
2743
|
+
const sessPeek = session;
|
|
2744
|
+
const realErr = sliceErr ?? (typeof sessPeek.peekRealError === "function" ? await sessPeek.peekRealError() : null);
|
|
2745
|
+
let errorRenderedHere = false;
|
|
2746
|
+
let walkedItems = 0;
|
|
2747
|
+
let syncsDrained = 0;
|
|
2748
|
+
let resultsDrained = 0;
|
|
2749
|
+
for (const r of settled) {
|
|
2750
|
+
walkedItems++;
|
|
2751
|
+
if (r.status !== "fulfilled") {
|
|
2752
|
+
resultsDrained++;
|
|
2753
|
+
if (!errorRenderedHere) {
|
|
2754
|
+
const reason = r.reason;
|
|
2755
|
+
if (typeof reason === "object" && reason !== null && reason.pipelineAborted === true && realErr !== null) renderPipelineError(realErr);
|
|
2756
|
+
else renderPipelineError(reason);
|
|
2757
|
+
errorRenderedHere = true;
|
|
2758
|
+
}
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
const rs = r.value;
|
|
2762
|
+
for (const n of rs.notices) {
|
|
2763
|
+
let out = `${n.severity}: ${n.message}\n`;
|
|
2764
|
+
if (n.detail !== void 0) out += `DETAIL: ${n.detail}\n`;
|
|
2765
|
+
if (n.hint !== void 0) out += `HINT: ${n.hint}\n`;
|
|
2766
|
+
writeErr(out);
|
|
2767
|
+
}
|
|
2768
|
+
if (rs.fields.length === 0 && rs.command === "" && rs.rows.length === 0) {
|
|
2769
|
+
syncsDrained++;
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
resultsDrained++;
|
|
2773
|
+
if (rs.fields.length === 0 && rs.rows.length > 0) {
|
|
2774
|
+
if (!ctx.settings.popt.topt.tuplesOnly) {
|
|
2775
|
+
process.stdout.write("--\n");
|
|
2776
|
+
process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
|
|
2777
|
+
}
|
|
2778
|
+
} else if (rs.fields.length > 0) await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
|
|
2779
|
+
}
|
|
2780
|
+
const sessMark = session;
|
|
2781
|
+
if (typeof sessMark.markDrained === "function") sessMark.markDrained(end);
|
|
2782
|
+
if (errorRenderedHere) {
|
|
2783
|
+
const sessAny = session;
|
|
2784
|
+
if (typeof sessAny.clearLastError === "function") sessAny.clearLastError();
|
|
2785
|
+
}
|
|
2786
|
+
if (walkedItems === 0) {
|
|
2787
|
+
const ddata = Math.min(n, resultAvailable);
|
|
2788
|
+
resultsDrained = ddata;
|
|
2789
|
+
syncsDrained = n - ddata;
|
|
2790
|
+
} else {
|
|
2791
|
+
const actualSyncs = Math.min(syncsDrained, syncAvailable);
|
|
2792
|
+
const extraResults = syncsDrained - actualSyncs;
|
|
2793
|
+
syncsDrained = actualSyncs;
|
|
2794
|
+
resultsDrained += extraResults;
|
|
2795
|
+
}
|
|
2796
|
+
bumpCounter(ctx.settings, "PIPELINE_SYNC_COUNT", -syncsDrained);
|
|
2797
|
+
bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", -resultsDrained);
|
|
2798
|
+
return { status: "ok" };
|
|
2799
|
+
} catch (err) {
|
|
2800
|
+
return errResult(ctx, errorToMessage(err));
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
const registerPipelineCommands = (registry) => {
|
|
2805
|
+
registry.register(cmdBind);
|
|
2806
|
+
registry.register(cmdBindNamed);
|
|
2807
|
+
registry.register(cmdParse);
|
|
2808
|
+
registry.register(cmdClosePrepared);
|
|
2809
|
+
registry.register(cmdStartPipeline);
|
|
2810
|
+
registry.register(cmdEndPipeline);
|
|
2811
|
+
registry.register(cmdSyncPipeline);
|
|
2812
|
+
registry.register(cmdFlushRequest);
|
|
2813
|
+
registry.register(cmdFlush);
|
|
2814
|
+
registry.register(cmdSendPipeline);
|
|
2815
|
+
registry.register(cmdGetResults);
|
|
2816
|
+
};
|
|
2817
|
+
//#endregion
|
|
2818
|
+
export { cmdPrint as A, psqlExec as B, cmdGdesc as C, cmdInclude as D, cmdGx as E, registerIoCommands as F, writeQueryError as G, renderResultSet as H, __testing as I, captureLastError as L, cmdWrite as M, formatWatchTimestamp as N, cmdIncludeRel as O, getQueryFout as P, executeAndPrint as R, cmdG as S, cmdGset as T, sendQuery as U, refreshErrorVars as V, stripLeadingCommentsAndWS as W, stagedNamedBindPresent as _, cmdFlush as a, WATCH_TEST_CONTROLLER as b, cmdParse as c, cmdSyncPipeline as d, consumeBindState as f, registerPipelineCommands as g, lookupPrepared as h, cmdEndPipeline as i, cmdWatch as j, cmdOut as k, cmdSendPipeline as l, getPipelineState as m, cmdBindNamed as n, cmdFlushRequest as o, dropPrepared as p, cmdClosePrepared as r, cmdGetResults as s, cmdBind as t, cmdStartPipeline as u, stashPrepared as v, cmdGexec as w, clearPipelineGateErrors as x, DEFAULT_WATCH_INTERVAL as y, pickOut$1 as z };
|