neon 2.47.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +395 -0
- package/dist/_shared/env-core/reuse-secrets.js +179 -0
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +575 -658
- 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 +642 -681
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -341
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -64
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +133 -147
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +9 -7
|
@@ -1,1148 +1,596 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* psql pipeline / extended-query backslash commands (WP-21).
|
|
3
|
-
*
|
|
4
|
-
* Implements the subset of upstream psql's "pipeline mode" backslash commands
|
|
5
|
-
* that drive the extended protocol directly. These are typically used together
|
|
6
|
-
* with the existing `\g` flow: a `\bind` (or `\parse`) command stashes
|
|
7
|
-
* parameter / statement state on the {@link BackslashContext}'s settings via
|
|
8
|
-
* a Symbol-keyed slot; when the mainloop then sees a `;` (or a `\g`), it can
|
|
9
|
-
* notice the stashed state and route the query through
|
|
10
|
-
* `Connection.query(sql, params)` instead of `execSimple`.
|
|
11
|
-
*
|
|
12
|
-
* Because the mainloop integration is owned by other WPs (and was deliberately
|
|
13
|
-
* left untouched here), this module exposes both the command specs AND a
|
|
14
|
-
* small helper, {@link getPipelineState}, that the mainloop will use to
|
|
15
|
-
* consult the stashed state. The commands operate on the buffered query the
|
|
16
|
-
* same way `\g` does — they execute or queue the buffer and reset it.
|
|
17
|
-
*
|
|
18
|
-
* Commands shipped:
|
|
19
|
-
*
|
|
20
|
-
* \bind [VALUE ...] stash params for next ; / \g
|
|
21
|
-
* \bind_named NAME [V ...] stash params + named prepared statement
|
|
22
|
-
* \parse NAME prepare current query buffer as NAME
|
|
23
|
-
* \close_prepared NAME Close('S', NAME)
|
|
24
|
-
* \startpipeline begin a pipeline session (settings.sendMode)
|
|
25
|
-
* \endpipeline end the pipeline session, drain results
|
|
26
|
-
* \syncpipeline send Sync mid-pipeline
|
|
27
|
-
* \sendpipeline submit the current buffered query w/o waiting
|
|
28
|
-
* \flushrequest send Flush
|
|
29
|
-
* \flush alias for \flushrequest
|
|
30
|
-
* \getresults [N] drain pending pipeline results
|
|
31
|
-
* \gdesc describe-without-execute the buffered query
|
|
32
|
-
*
|
|
33
|
-
* The set is registered in bulk via {@link registerPipelineCommands}.
|
|
34
|
-
*/
|
|
35
|
-
import { alignedPrinter } from "../print/aligned.js";
|
|
36
|
-
import { clearPipelineGateErrors } from "./cmd_io.js";
|
|
37
1
|
import { writeErr } from "./shared.js";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
2
|
+
import { alignedPrinter } from "../print/aligned.js";
|
|
3
|
+
import { clearPipelineGateErrors, cmdGdesc } from "./cmd_io.js";
|
|
4
|
+
//#region src/psql/command/cmd_pipeline.ts
|
|
5
|
+
/**
|
|
6
|
+
* psql pipeline / extended-query backslash commands (WP-21).
|
|
7
|
+
*
|
|
8
|
+
* Implements the subset of upstream psql's "pipeline mode" backslash commands
|
|
9
|
+
* that drive the extended protocol directly. These are typically used together
|
|
10
|
+
* with the existing `\g` flow: a `\bind` (or `\parse`) command stashes
|
|
11
|
+
* parameter / statement state on the {@link BackslashContext}'s settings via
|
|
12
|
+
* a Symbol-keyed slot; when the mainloop then sees a `;` (or a `\g`), it can
|
|
13
|
+
* notice the stashed state and route the query through
|
|
14
|
+
* `Connection.query(sql, params)` instead of `execSimple`.
|
|
15
|
+
*
|
|
16
|
+
* Because the mainloop integration is owned by other WPs (and was deliberately
|
|
17
|
+
* left untouched here), this module exposes both the command specs AND a
|
|
18
|
+
* small helper, {@link getPipelineState}, that the mainloop will use to
|
|
19
|
+
* consult the stashed state. The commands operate on the buffered query the
|
|
20
|
+
* same way `\g` does — they execute or queue the buffer and reset it.
|
|
21
|
+
*
|
|
22
|
+
* Commands shipped:
|
|
23
|
+
*
|
|
24
|
+
* \bind [VALUE ...] stash params for next ; / \g
|
|
25
|
+
* \bind_named NAME [V ...] stash params + named prepared statement
|
|
26
|
+
* \parse NAME prepare current query buffer as NAME
|
|
27
|
+
* \close_prepared NAME Close('S', NAME)
|
|
28
|
+
* \startpipeline begin a pipeline session (settings.sendMode)
|
|
29
|
+
* \endpipeline end the pipeline session, drain results
|
|
30
|
+
* \syncpipeline send Sync mid-pipeline
|
|
31
|
+
* \sendpipeline submit the current buffered query w/o waiting
|
|
32
|
+
* \flushrequest send Flush
|
|
33
|
+
* \flush alias for \flushrequest
|
|
34
|
+
* \getresults [N] drain pending pipeline results
|
|
35
|
+
* \gdesc describe-without-execute the buffered query
|
|
36
|
+
*
|
|
37
|
+
* The set is registered in bulk via {@link registerPipelineCommands}.
|
|
38
|
+
*/
|
|
42
39
|
const BIND_STATE_KEY = Symbol.for("neonctl.psql.bindState");
|
|
43
40
|
const PIPELINE_KEY = Symbol.for("neonctl.psql.pipeline");
|
|
44
41
|
const PREPARED_BY_NAME_KEY = Symbol.for("neonctl.psql.preparedByName");
|
|
45
42
|
const stashOf = (settings) => settings;
|
|
46
43
|
/** Read (and clear) the pending bind params, if any. */
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
const consumeBindState = (settings) => {
|
|
45
|
+
const s = stashOf(settings);
|
|
46
|
+
const cur = s[BIND_STATE_KEY] ?? null;
|
|
47
|
+
s[BIND_STATE_KEY] = void 0;
|
|
48
|
+
return cur;
|
|
52
49
|
};
|
|
53
50
|
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
* Peek at the bind stash without consuming. Used by `\g` to decide
|
|
52
|
+
* whether to skip the "empty buffer, no prior query" no-op guard:
|
|
53
|
+
* when a `\bind_named NAME` is pending, the prepared statement carries
|
|
54
|
+
* the SQL server-side so no buffer text is needed.
|
|
55
|
+
*/
|
|
56
|
+
const stagedNamedBindPresent = (settings) => {
|
|
57
|
+
const cur = stashOf(settings)[BIND_STATE_KEY];
|
|
58
|
+
return !!cur && cur.byName;
|
|
62
59
|
};
|
|
63
60
|
/** Stash a PreparedStatement for later `\bind_named NAME \g` lookup. */
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
const stashPrepared = (settings, name, ps) => {
|
|
62
|
+
const s = stashOf(settings);
|
|
63
|
+
let map = s[PREPARED_BY_NAME_KEY];
|
|
64
|
+
if (!map) {
|
|
65
|
+
map = /* @__PURE__ */ new Map();
|
|
66
|
+
s[PREPARED_BY_NAME_KEY] = map;
|
|
67
|
+
}
|
|
68
|
+
map.set(name, ps);
|
|
72
69
|
};
|
|
73
70
|
/** Look up a previously-stashed PreparedStatement by name. */
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return map?.get(name) ?? null;
|
|
71
|
+
const lookupPrepared = (settings, name) => {
|
|
72
|
+
return stashOf(settings)[PREPARED_BY_NAME_KEY]?.get(name) ?? null;
|
|
77
73
|
};
|
|
78
74
|
/** Drop a stashed PreparedStatement (after `\close_prepared`). */
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
map.delete(name);
|
|
75
|
+
const dropPrepared = (settings, name) => {
|
|
76
|
+
const map = stashOf(settings)[PREPARED_BY_NAME_KEY];
|
|
77
|
+
if (map) map.delete(name);
|
|
83
78
|
};
|
|
84
79
|
/** Peek at the current pipeline session (or null). */
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// PIPELINE_* counter book-keeping.
|
|
88
|
-
//
|
|
89
|
-
// Upstream psql 18 exposes three pipeline counters as `:VAR`-interpolatable
|
|
90
|
-
// session variables (initialized to "0" in `settings.ts` at startup):
|
|
91
|
-
//
|
|
92
|
-
// PIPELINE_COMMAND_COUNT number of P/B/E batches queued since last Sync
|
|
93
|
-
// PIPELINE_SYNC_COUNT number of Syncs issued in the current pipeline
|
|
94
|
-
// PIPELINE_RESULT_COUNT results queued server-side but not yet fetched
|
|
95
|
-
//
|
|
96
|
-
// The counter rules (verified empirically against vanilla psql 18.4):
|
|
97
|
-
//
|
|
98
|
-
// \startpipeline — all three reset to "0".
|
|
99
|
-
// \parse — COMMAND_COUNT++.
|
|
100
|
-
// \sendpipeline — COMMAND_COUNT++.
|
|
101
|
-
// ;-query in pipeline mode — COMMAND_COUNT++ (each implicit-`;` send).
|
|
102
|
-
// \syncpipeline — SYNC_COUNT++, RESULT_COUNT += COMMAND_COUNT,
|
|
103
|
-
// COMMAND_COUNT = 0.
|
|
104
|
-
// \flushrequest /
|
|
105
|
-
// \flush — RESULT_COUNT += COMMAND_COUNT, COMMAND_COUNT = 0.
|
|
106
|
-
// \getresults [N] — RESULT_COUNT -= actually-drained; if RESULT_COUNT
|
|
107
|
-
// hits 0, SYNC_COUNT also resets to 0 (full-drain
|
|
108
|
-
// returns the pipeline to a clean slate).
|
|
109
|
-
// \endpipeline — all three reset to "0".
|
|
110
|
-
//
|
|
111
|
-
// For `;`-queries the increment fires from a wrapper installed around the
|
|
112
|
-
// stashed `Pipeline.execute` in `cmdStartPipeline.run` — mainloop's
|
|
113
|
-
// `dispatchSendQuery` calls `ps.session.parse/bind/execute` directly, and
|
|
114
|
-
// the wrapper picks that up without mainloop having to know about the var
|
|
115
|
-
// store.
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
80
|
+
const getPipelineState = (settings) => stashOf(settings)[PIPELINE_KEY] ?? null;
|
|
117
81
|
const readCounter = (settings, name) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
82
|
+
const raw = settings.vars.get(name);
|
|
83
|
+
if (raw === void 0) return 0;
|
|
84
|
+
const n = parseInt(raw, 10);
|
|
85
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
123
86
|
};
|
|
124
87
|
const setCounter = (settings, name, value) => {
|
|
125
|
-
|
|
88
|
+
settings.vars.set(name, String(Math.max(0, value)));
|
|
126
89
|
};
|
|
127
90
|
const bumpCounter = (settings, name, delta) => {
|
|
128
|
-
|
|
91
|
+
setCounter(settings, name, readCounter(settings, name) + delta);
|
|
129
92
|
};
|
|
130
93
|
const resetPipelineCounters = (settings) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
94
|
+
setCounter(settings, "PIPELINE_COMMAND_COUNT", 0);
|
|
95
|
+
setCounter(settings, "PIPELINE_SYNC_COUNT", 0);
|
|
96
|
+
setCounter(settings, "PIPELINE_RESULT_COUNT", 0);
|
|
134
97
|
};
|
|
135
98
|
/**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
99
|
+
* Wrap `Pipeline.execute` so each enqueued Execute message bumps
|
|
100
|
+
* `PIPELINE_COMMAND_COUNT`. The wrapping covers both call sites:
|
|
101
|
+
*
|
|
102
|
+
* - `cmdSendPipeline` (this file) — `\sendpipeline`.
|
|
103
|
+
* - `dispatchSendQuery` (mainloop) — implicit `;`-queries while pipeline
|
|
104
|
+
* is active. Mainloop calls `ps.session.execute('', 0)` so the wrapper
|
|
105
|
+
* fires automatically without mainloop knowing about the var store.
|
|
106
|
+
*
|
|
107
|
+
* The wrapper preserves the original function's `this` binding via `apply`.
|
|
108
|
+
*/
|
|
146
109
|
const wrapSessionForCounters = (session, settings) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
110
|
+
const origExecute = session.execute.bind(session);
|
|
111
|
+
session.execute = (name, maxRows) => {
|
|
112
|
+
bumpCounter(settings, "PIPELINE_COMMAND_COUNT", 1);
|
|
113
|
+
return origExecute(name, maxRows);
|
|
114
|
+
};
|
|
115
|
+
return session;
|
|
153
116
|
};
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
// Helpers
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
117
|
const errResult = (ctx, message) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
118
|
+
ctx.settings.lastErrorResult = { message };
|
|
119
|
+
writeErr(`\\${ctx.cmdName}: ${message}\n`);
|
|
120
|
+
return {
|
|
121
|
+
status: "error",
|
|
122
|
+
errorWritten: true
|
|
123
|
+
};
|
|
163
124
|
};
|
|
164
125
|
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
126
|
+
* Coerce any thrown / rejected value into a printable string. The
|
|
127
|
+
* extended-protocol driver in `PgConnection` rejects with raw
|
|
128
|
+
* ConnectError records (`{severity, code, message, detail, ...}`) —
|
|
129
|
+
* not `Error` instances — so `String(err)` would produce
|
|
130
|
+
* `[object Object]` in the conformance output. We probe for a
|
|
131
|
+
* `.message` property (covering both `Error` and the ConnectError
|
|
132
|
+
* shape) and fall back to JSON.stringify only when there's no
|
|
133
|
+
* message field at all.
|
|
134
|
+
*/
|
|
174
135
|
const errorToMessage = (err) => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
return JSON.stringify(err);
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
return String(err);
|
|
188
|
-
}
|
|
136
|
+
if (err instanceof Error) return err.message;
|
|
137
|
+
if (err !== null && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message;
|
|
138
|
+
try {
|
|
139
|
+
return JSON.stringify(err);
|
|
140
|
+
} catch {
|
|
141
|
+
return String(err);
|
|
142
|
+
}
|
|
189
143
|
};
|
|
190
144
|
/**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
145
|
+
* Render a pipeline-collected ConnectError to stderr in the upstream
|
|
146
|
+
* shape: a `SEVERITY: message` line, followed by optional `DETAIL:` /
|
|
147
|
+
* `HINT:` / `CONTEXT:` lines (each on its own line). Matches the
|
|
148
|
+
* subset of `formatErrorReport`-style output the conformance corpus
|
|
149
|
+
* expects from `\endpipeline` for non-FATAL pipeline errors. We
|
|
150
|
+
* inline a minimal renderer here rather than importing the full
|
|
151
|
+
* `formatErrorReport` from `cmd_meta` because we always want the
|
|
152
|
+
* "default verbosity, no LINE/caret context" form — pipeline errors
|
|
153
|
+
* arrive after the buffered query has been reset and we don't carry
|
|
154
|
+
* the originating SQL position through `session.lastError`.
|
|
155
|
+
*/
|
|
202
156
|
const renderPipelineError = (err) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const message = e.message ?? "";
|
|
219
|
-
writeErr(`${severity}: ${message}\n`);
|
|
220
|
-
if (e.detail)
|
|
221
|
-
writeErr(`DETAIL: ${e.detail}\n`);
|
|
222
|
-
if (e.hint)
|
|
223
|
-
writeErr(`HINT: ${e.hint}\n`);
|
|
224
|
-
if (e.where)
|
|
225
|
-
writeErr(`CONTEXT: ${e.where}\n`);
|
|
157
|
+
if (err === null || typeof err !== "object") {
|
|
158
|
+
writeErr(`ERROR: ${errorToMessage(err)}\n`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const e = err;
|
|
162
|
+
if (e.pipelineAborted) {
|
|
163
|
+
writeErr(`${e.message ?? "Pipeline aborted, command did not run"}\n`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const severity = e.severity ?? "ERROR";
|
|
167
|
+
const message = e.message ?? "";
|
|
168
|
+
writeErr(`${severity}: ${message}\n`);
|
|
169
|
+
if (e.detail) writeErr(`DETAIL: ${e.detail}\n`);
|
|
170
|
+
if (e.hint) writeErr(`HINT: ${e.hint}\n`);
|
|
171
|
+
if (e.where) writeErr(`CONTEXT: ${e.where}\n`);
|
|
226
172
|
};
|
|
227
173
|
const readAllArgs = (ctx) => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return out;
|
|
174
|
+
const out = [];
|
|
175
|
+
for (;;) {
|
|
176
|
+
const arg = ctx.nextArg("normal");
|
|
177
|
+
if (arg === null) break;
|
|
178
|
+
out.push(arg);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
236
181
|
};
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
};
|
|
250
|
-
return Promise.resolve({ status: "ok" });
|
|
251
|
-
},
|
|
182
|
+
const cmdBind = {
|
|
183
|
+
name: "bind",
|
|
184
|
+
helpKey: "bind",
|
|
185
|
+
run(ctx) {
|
|
186
|
+
const values = readAllArgs(ctx);
|
|
187
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = {
|
|
188
|
+
name: "",
|
|
189
|
+
values,
|
|
190
|
+
byName: false
|
|
191
|
+
};
|
|
192
|
+
return Promise.resolve({ status: "ok" });
|
|
193
|
+
}
|
|
252
194
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
const values = readAllArgs(ctx);
|
|
272
|
-
stashOf(ctx.settings)[BIND_STATE_KEY] = { name, values, byName: true };
|
|
273
|
-
return Promise.resolve({ status: "ok" });
|
|
274
|
-
},
|
|
195
|
+
const cmdBindNamed = {
|
|
196
|
+
name: "bind_named",
|
|
197
|
+
helpKey: "bind_named",
|
|
198
|
+
run(ctx) {
|
|
199
|
+
const name = ctx.nextArg("normal");
|
|
200
|
+
if (name === null) {
|
|
201
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = void 0;
|
|
202
|
+
return Promise.resolve(errResult(ctx, "missing required argument"));
|
|
203
|
+
}
|
|
204
|
+
const values = readAllArgs(ctx);
|
|
205
|
+
stashOf(ctx.settings)[BIND_STATE_KEY] = {
|
|
206
|
+
name,
|
|
207
|
+
values,
|
|
208
|
+
byName: true
|
|
209
|
+
};
|
|
210
|
+
return Promise.resolve({ status: "ok" });
|
|
211
|
+
}
|
|
275
212
|
};
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// expects the third Parse's `could not determine data type` error
|
|
312
|
-
// to surface AT `\endpipeline` time, not synchronously here.
|
|
313
|
-
const pipelineActive = getPipelineState(ctx.settings);
|
|
314
|
-
if (pipelineActive !== null) {
|
|
315
|
-
try {
|
|
316
|
-
// `\parse NAME` is a USER-level command (one entry on libpq's
|
|
317
|
-
// result queue), so route through `parseSlot` (real
|
|
318
|
-
// PipelineSession) which both enqueues the Parse wire op AND
|
|
319
|
-
// registers a `cmdSlots` entry. `\getresults` walks `cmdSlots`,
|
|
320
|
-
// so without the slot the cmd would be invisible to drain
|
|
321
|
-
// accounting. Test mocks that don't implement `parseSlot` fall
|
|
322
|
-
// through to the plain `parse()` method.
|
|
323
|
-
const session = pipelineActive.session;
|
|
324
|
-
if (typeof session.parseSlot === "function") {
|
|
325
|
-
await session.parseSlot(name, sql, []);
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
await session.parse(name, sql, []);
|
|
329
|
-
}
|
|
330
|
-
ctx.settings.lastQuery = sql;
|
|
331
|
-
// Upstream `exec_command_parse` bumps `pset.piped_commands` after
|
|
332
|
-
// PQsendPrepare succeeds — the Parse is one queued command.
|
|
333
|
-
bumpCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 1);
|
|
334
|
-
return { status: "reset-buf", newBuf: "" };
|
|
335
|
-
}
|
|
336
|
-
catch (err) {
|
|
337
|
-
return errResult(ctx, errorToMessage(err));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
try {
|
|
341
|
-
const ps = await ctx.settings.db.prepare(name, sql);
|
|
342
|
-
// Cache for `\bind_named NAME \g` lookup later. Upstream tracks
|
|
343
|
-
// server-side prepared statements by name in `pset.psqlScanState`-
|
|
344
|
-
// adjacent state; we keep a local map so a follow-on `\g` can
|
|
345
|
-
// bind + execute without re-parsing.
|
|
346
|
-
stashPrepared(ctx.settings, name, ps);
|
|
347
|
-
// Upstream `exec_command_parse` also updates `pset.last_query` to
|
|
348
|
-
// the prepared SQL so a subsequent `\g` (e.g. after a failed
|
|
349
|
-
// `\bind_named NAME` that wipes bind state) re-runs the parsed
|
|
350
|
-
// text via the simple-query path. Without this, our `\g` would
|
|
351
|
-
// either no-op or fall back to a stale prior query, missing the
|
|
352
|
-
// "ERROR: there is no parameter $1" the conformance corpus
|
|
353
|
-
// expects from `SELECT $1, $2` executed without bind params.
|
|
354
|
-
ctx.settings.lastQuery = sql;
|
|
355
|
-
return { status: "reset-buf", newBuf: "" };
|
|
356
|
-
}
|
|
357
|
-
catch (err) {
|
|
358
|
-
return errResult(ctx, errorToMessage(err));
|
|
359
|
-
}
|
|
360
|
-
},
|
|
213
|
+
const cmdParse = {
|
|
214
|
+
name: "parse",
|
|
215
|
+
helpKey: "parse",
|
|
216
|
+
async run(ctx) {
|
|
217
|
+
const name = ctx.nextArg("normal");
|
|
218
|
+
if (name === null) return errResult(ctx, "missing required argument");
|
|
219
|
+
const sql = ctx.queryBuf;
|
|
220
|
+
if (sql.trim().length === 0) return errResult(ctx, "no query buffer");
|
|
221
|
+
if (!ctx.settings.db) return errResult(ctx, "no connection to the server");
|
|
222
|
+
const pipelineActive = getPipelineState(ctx.settings);
|
|
223
|
+
if (pipelineActive !== null) try {
|
|
224
|
+
const session = pipelineActive.session;
|
|
225
|
+
if (typeof session.parseSlot === "function") await session.parseSlot(name, sql, []);
|
|
226
|
+
else await session.parse(name, sql, []);
|
|
227
|
+
ctx.settings.lastQuery = sql;
|
|
228
|
+
bumpCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 1);
|
|
229
|
+
return {
|
|
230
|
+
status: "reset-buf",
|
|
231
|
+
newBuf: ""
|
|
232
|
+
};
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return errResult(ctx, errorToMessage(err));
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const ps = await ctx.settings.db.prepare(name, sql);
|
|
238
|
+
stashPrepared(ctx.settings, name, ps);
|
|
239
|
+
ctx.settings.lastQuery = sql;
|
|
240
|
+
return {
|
|
241
|
+
status: "reset-buf",
|
|
242
|
+
newBuf: ""
|
|
243
|
+
};
|
|
244
|
+
} catch (err) {
|
|
245
|
+
return errResult(ctx, errorToMessage(err));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
361
248
|
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// (see the parseSlot comment in `\parse` above). Test mocks
|
|
391
|
-
// that don't implement `closeSlot` fall through to plain
|
|
392
|
-
// `close()`.
|
|
393
|
-
const session = ps.session;
|
|
394
|
-
if (typeof session.closeSlot === "function") {
|
|
395
|
-
await session.closeSlot(name);
|
|
396
|
-
}
|
|
397
|
-
else {
|
|
398
|
-
await session.close(name);
|
|
399
|
-
}
|
|
400
|
-
// Upstream `exec_command_close_prepared` bumps `piped_commands`
|
|
401
|
-
// (PIPELINE_COMMAND_COUNT) after a successful PQsendClosePrepared.
|
|
402
|
-
bumpCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 1);
|
|
403
|
-
// Drop any cached binding so a later `\bind_named NAME \g`
|
|
404
|
-
// errors cleanly instead of using a stale handle.
|
|
405
|
-
dropPrepared(ctx.settings, name);
|
|
406
|
-
return { status: "ok" };
|
|
407
|
-
}
|
|
408
|
-
catch (err) {
|
|
409
|
-
return errResult(ctx, errorToMessage(err));
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
try {
|
|
413
|
-
// Out-of-pipeline path: upstream issues `Close('S', name) + Sync`
|
|
414
|
-
// directly; the server treats Close on a missing name as a no-op
|
|
415
|
-
// (CloseComplete without diagnostics), so we don't need to know
|
|
416
|
-
// whether the statement exists. A previous implementation faked a
|
|
417
|
-
// `prepare(name, 'SELECT 1')` to reach the same Close, which broke
|
|
418
|
-
// when the name was already prepared on the server (Parse fails
|
|
419
|
-
// with `prepared statement "NAME" already exists`).
|
|
420
|
-
if (db.closePreparedStatement) {
|
|
421
|
-
await db.closePreparedStatement(name);
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
// Backwards-compat path for Connection mocks that don't
|
|
425
|
-
// implement the dedicated entry point. The real PgConnection
|
|
426
|
-
// always provides closePreparedStatement; this branch only
|
|
427
|
-
// fires under unit tests with bespoke Connection mocks.
|
|
428
|
-
const stmt = await db.prepare(name, "SELECT 1");
|
|
429
|
-
await stmt.close();
|
|
430
|
-
}
|
|
431
|
-
// Drop any cached binding so a later `\bind_named NAME \g` errors
|
|
432
|
-
// cleanly instead of using a stale handle.
|
|
433
|
-
dropPrepared(ctx.settings, name);
|
|
434
|
-
return { status: "ok" };
|
|
435
|
-
}
|
|
436
|
-
catch (err) {
|
|
437
|
-
return errResult(ctx, errorToMessage(err));
|
|
438
|
-
}
|
|
439
|
-
},
|
|
249
|
+
const cmdClosePrepared = {
|
|
250
|
+
name: "close_prepared",
|
|
251
|
+
helpKey: "close_prepared",
|
|
252
|
+
async run(ctx) {
|
|
253
|
+
const name = ctx.nextArg("normal");
|
|
254
|
+
if (name === null) return errResult(ctx, "missing required argument");
|
|
255
|
+
const db = ctx.settings.db;
|
|
256
|
+
if (!db) return errResult(ctx, "no connection to the server");
|
|
257
|
+
const ps = getPipelineState(ctx.settings);
|
|
258
|
+
if (ps !== null) try {
|
|
259
|
+
const session = ps.session;
|
|
260
|
+
if (typeof session.closeSlot === "function") await session.closeSlot(name);
|
|
261
|
+
else await session.close(name);
|
|
262
|
+
bumpCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 1);
|
|
263
|
+
dropPrepared(ctx.settings, name);
|
|
264
|
+
return { status: "ok" };
|
|
265
|
+
} catch (err) {
|
|
266
|
+
return errResult(ctx, errorToMessage(err));
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
if (db.closePreparedStatement) await db.closePreparedStatement(name);
|
|
270
|
+
else await (await db.prepare(name, "SELECT 1")).close();
|
|
271
|
+
dropPrepared(ctx.settings, name);
|
|
272
|
+
return { status: "ok" };
|
|
273
|
+
} catch (err) {
|
|
274
|
+
return errResult(ctx, errorToMessage(err));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
440
277
|
};
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const session = wrapSessionForCounters(ctx.settings.db.pipeline(), ctx.settings);
|
|
462
|
-
stashOf(ctx.settings)[PIPELINE_KEY] = {
|
|
463
|
-
session,
|
|
464
|
-
pending: [],
|
|
465
|
-
drainedCount: 0,
|
|
466
|
-
};
|
|
467
|
-
ctx.settings.sendMode = "extended-pipeline";
|
|
468
|
-
// Upstream `exec_command_startpipeline` calls SetVariable for the three
|
|
469
|
-
// counter vars (zeroing whatever the prior pipeline left behind).
|
|
470
|
-
resetPipelineCounters(ctx.settings);
|
|
471
|
-
return Promise.resolve({ status: "ok" });
|
|
472
|
-
}
|
|
473
|
-
catch (err) {
|
|
474
|
-
return Promise.resolve(errResult(ctx, errorToMessage(err)));
|
|
475
|
-
}
|
|
476
|
-
},
|
|
278
|
+
const cmdStartPipeline = {
|
|
279
|
+
name: "startpipeline",
|
|
280
|
+
helpKey: "startpipeline",
|
|
281
|
+
run(ctx) {
|
|
282
|
+
if (!ctx.settings.db) return Promise.resolve(errResult(ctx, "no connection to the server"));
|
|
283
|
+
if (getPipelineState(ctx.settings) !== null) return Promise.resolve({ status: "ok" });
|
|
284
|
+
try {
|
|
285
|
+
const session = wrapSessionForCounters(ctx.settings.db.pipeline(), ctx.settings);
|
|
286
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = {
|
|
287
|
+
session,
|
|
288
|
+
pending: [],
|
|
289
|
+
drainedCount: 0
|
|
290
|
+
};
|
|
291
|
+
ctx.settings.sendMode = "extended-pipeline";
|
|
292
|
+
resetPipelineCounters(ctx.settings);
|
|
293
|
+
return Promise.resolve({ status: "ok" });
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return Promise.resolve(errResult(ctx, errorToMessage(err)));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
477
298
|
};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
// lives in `pending` (Parse / Bind / Close — none of which are
|
|
566
|
-
// tracked on `cmdSlots` as a separate slot), the slot inherits
|
|
567
|
-
// the cascaded marker — in that case fall through to
|
|
568
|
-
// `session.lastError` which captures the original ERROR from the
|
|
569
|
-
// wire-layer `sync()` / `end()` path.
|
|
570
|
-
const sliceForError = entries.slice(alreadyDrained);
|
|
571
|
-
const realFromSlice = (() => {
|
|
572
|
-
for (const r of sliceForError) {
|
|
573
|
-
if (r.status !== "rejected")
|
|
574
|
-
continue;
|
|
575
|
-
const reason = r.reason;
|
|
576
|
-
const isAborted = typeof reason === "object" &&
|
|
577
|
-
reason !== null &&
|
|
578
|
-
reason
|
|
579
|
-
.pipelineAborted === true;
|
|
580
|
-
if (!isAborted)
|
|
581
|
-
return reason;
|
|
582
|
-
}
|
|
583
|
-
return null;
|
|
584
|
-
})();
|
|
585
|
-
const realLastError = realFromSlice ??
|
|
586
|
-
(() => {
|
|
587
|
-
const le = session.lastError;
|
|
588
|
-
if (le === null || le === undefined)
|
|
589
|
-
return null;
|
|
590
|
-
if (typeof le === "object" &&
|
|
591
|
-
le.pipelineAborted) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
return le;
|
|
595
|
-
})();
|
|
596
|
-
for (let i = alreadyDrained; i < entries.length; i++) {
|
|
597
|
-
const r = entries[i];
|
|
598
|
-
if (r.status === "fulfilled") {
|
|
599
|
-
const rs = r.value;
|
|
600
|
-
// Emit any NoticeResponse messages attached to this result
|
|
601
|
-
// to stderr in the upstream libpq shape
|
|
602
|
-
// (`${severity}: ${message}\n`). In pipeline mode the
|
|
603
|
-
// server emits notices interleaved with Bind / Execute
|
|
604
|
-
// replies, so they're attached to the corresponding
|
|
605
|
-
// ResultSet's `notices` array; vanilla psql 18.4 prints
|
|
606
|
-
// them BEFORE the result body at `\endpipeline` time
|
|
607
|
-
// (e.g. `regress/psql_pipeline.out` line 671: the
|
|
608
|
-
// `WARNING: SET LOCAL can only be used in transaction
|
|
609
|
-
// blocks` lands right before the first `statement_timeout`
|
|
610
|
-
// table).
|
|
611
|
-
for (const n of rs.notices) {
|
|
612
|
-
let out = `${n.severity}: ${n.message}\n`;
|
|
613
|
-
if (n.detail !== undefined)
|
|
614
|
-
out += `DETAIL: ${n.detail}\n`;
|
|
615
|
-
if (n.hint !== undefined)
|
|
616
|
-
out += `HINT: ${n.hint}\n`;
|
|
617
|
-
writeErr(out);
|
|
618
|
-
}
|
|
619
|
-
// Print real tuples-producing results — including the 0-column
|
|
620
|
-
// 1-row shape from `SELECT \bind \sendpipeline` which upstream
|
|
621
|
-
// psql renders as `--\n(1 row)\n` (the table glyphs are just
|
|
622
|
-
// the trailing separator row plus the default row-count footer).
|
|
623
|
-
// Skip our internal Sync marker (empty `command`, see
|
|
624
|
-
// wire/pipeline.ts) and DDL-style CommandComplete-only sets
|
|
625
|
-
// (non-empty `command` but no fields and no rows).
|
|
626
|
-
const isSyncOrPlaceholder = rs.fields.length === 0 &&
|
|
627
|
-
rs.command === "" &&
|
|
628
|
-
rs.rows.length === 0;
|
|
629
|
-
const isCommandOnly = rs.fields.length === 0 &&
|
|
630
|
-
rs.rows.length === 0 &&
|
|
631
|
-
rs.command !== "";
|
|
632
|
-
if (!isSyncOrPlaceholder && !isCommandOnly) {
|
|
633
|
-
if (rs.fields.length === 0 && rs.rows.length > 0) {
|
|
634
|
-
// 0-column tuples result: the aligned printer's
|
|
635
|
-
// header/rule machinery degenerates to whitespace because
|
|
636
|
-
// there are no column widths to drive the dividers. Emit
|
|
637
|
-
// the upstream-shaped placeholder (`--` separator + row
|
|
638
|
-
// count) inline so we match `psql_pipeline.out`'s
|
|
639
|
-
// `\watch`-rejected SELECT output byte-for-byte.
|
|
640
|
-
const tuplesOnly = ctx.settings.popt.topt.tuplesOnly;
|
|
641
|
-
if (!tuplesOnly) {
|
|
642
|
-
process.stdout.write("--\n");
|
|
643
|
-
process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
else if (!errorRendered) {
|
|
652
|
-
// Render only the FIRST rejection inline — subsequent ops
|
|
653
|
-
// in an aborted pipeline reject with the synthetic
|
|
654
|
-
// `pipelineAborted` marker which we coalesce to one line.
|
|
655
|
-
// When the wire layer cascade-rejected from a Parse / Bind /
|
|
656
|
-
// Close that lives in `pending`, the only entry visible here
|
|
657
|
-
// is one rejected with `pipelineAborted` — fall back to
|
|
658
|
-
// `session.lastError` / `peekRealError` for the original
|
|
659
|
-
// ERROR.
|
|
660
|
-
const reason = r.reason;
|
|
661
|
-
const isAborted = typeof reason === "object" &&
|
|
662
|
-
reason !== null &&
|
|
663
|
-
reason
|
|
664
|
-
.pipelineAborted === true;
|
|
665
|
-
// FETCH_COUNT-in-pipeline: upstream emits the chunked-mode
|
|
666
|
-
// diagnostic in addition to the per-op line. Both go to
|
|
667
|
-
// stderr; the chunked-mode line comes FIRST and addresses the
|
|
668
|
-
// SQL-shaped failure (libpq's PQsetSingleRowMode rejection
|
|
669
|
-
// inside a pipeline). Mirror that for any rejection that
|
|
670
|
-
// surfaces at `\endpipeline` time while FETCH_COUNT was set.
|
|
671
|
-
// The chunked-mode prefix is gated on FETCH_COUNT, but the REAL
|
|
672
|
-
// ERROR must still be surfaced — the old code printed the prefix +
|
|
673
|
-
// "Pipeline aborted" for ANY rejection under FETCH_COUNT, swallowing
|
|
674
|
-
// the actual ERROR: text (review: minor divergences). "Pipeline
|
|
675
|
-
// aborted, command did not run" is shown ONLY for the synthetic
|
|
676
|
-
// queue-skip marker (no real error behind it).
|
|
677
|
-
if (fetchCountActive) {
|
|
678
|
-
writeErr("fetching results in chunked mode failed\n");
|
|
679
|
-
}
|
|
680
|
-
if (isAborted && realLastError !== null) {
|
|
681
|
-
renderPipelineError(realLastError);
|
|
682
|
-
}
|
|
683
|
-
else if (isAborted) {
|
|
684
|
-
writeErr("Pipeline aborted, command did not run\n");
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
renderPipelineError(reason);
|
|
688
|
-
}
|
|
689
|
-
errorRendered = true;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
// Fallback: if `session.lastError` was set but no per-op
|
|
693
|
-
// rejection was observed (can happen when the error came from
|
|
694
|
-
// the trailing Sync rather than a specific Execute), render it
|
|
695
|
-
// here. Otherwise the diagnostic would be lost.
|
|
696
|
-
if (!errorRendered) {
|
|
697
|
-
const lastErr = session.lastError;
|
|
698
|
-
if (lastErr !== null && lastErr !== undefined) {
|
|
699
|
-
// A real trailing-Sync error: name the chunked-mode failure when
|
|
700
|
-
// FETCH_COUNT was active, but still render the actual ERROR rather
|
|
701
|
-
// than the synthetic "Pipeline aborted" line (review: minor).
|
|
702
|
-
if (fetchCountActive) {
|
|
703
|
-
writeErr("fetching results in chunked mode failed\n");
|
|
704
|
-
}
|
|
705
|
-
renderPipelineError(lastErr);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
return { status: "ok" };
|
|
709
|
-
}
|
|
710
|
-
catch (err) {
|
|
711
|
-
stashOf(ctx.settings)[PIPELINE_KEY] = undefined;
|
|
712
|
-
ctx.settings.sendMode = "extended-query";
|
|
713
|
-
// Even on a failed `\endpipeline` (e.g. server hung up mid-drain),
|
|
714
|
-
// mirror upstream and clear the counters — the pipeline state is
|
|
715
|
-
// gone, so any non-zero value would be misleading. Same for the
|
|
716
|
-
// pipeline-gate error log.
|
|
717
|
-
resetPipelineCounters(ctx.settings);
|
|
718
|
-
clearPipelineGateErrors(ctx.settings);
|
|
719
|
-
return errResult(ctx, errorToMessage(err));
|
|
720
|
-
}
|
|
721
|
-
},
|
|
299
|
+
const cmdEndPipeline = {
|
|
300
|
+
name: "endpipeline",
|
|
301
|
+
helpKey: "endpipeline",
|
|
302
|
+
async run(ctx) {
|
|
303
|
+
const ps = getPipelineState(ctx.settings);
|
|
304
|
+
if (!ps) {
|
|
305
|
+
ctx.settings.lastErrorResult = { message: "cannot send pipeline when not in pipeline mode" };
|
|
306
|
+
writeErr("cannot send pipeline when not in pipeline mode\n");
|
|
307
|
+
return {
|
|
308
|
+
status: "error",
|
|
309
|
+
errorWritten: true
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const alreadyDrained = ps.drainedCount;
|
|
314
|
+
const session = ps.session;
|
|
315
|
+
const cmdSlotsSnapshot = Array.isArray(session.cmdSlots) ? session.cmdSlots.slice() : [];
|
|
316
|
+
const fetchCountActive = (ctx.settings.vars.get("FETCH_COUNT") ?? "0") !== "0";
|
|
317
|
+
const sets = await ps.session.end();
|
|
318
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = void 0;
|
|
319
|
+
ctx.settings.sendMode = "extended-query";
|
|
320
|
+
resetPipelineCounters(ctx.settings);
|
|
321
|
+
clearPipelineGateErrors(ctx.settings);
|
|
322
|
+
const settled = await Promise.allSettled(cmdSlotsSnapshot);
|
|
323
|
+
let errorRendered = false;
|
|
324
|
+
const entries = settled.length > 0 ? settled : sets.map((rs) => ({
|
|
325
|
+
status: "fulfilled",
|
|
326
|
+
value: rs
|
|
327
|
+
}));
|
|
328
|
+
const sliceForError = entries.slice(alreadyDrained);
|
|
329
|
+
const realLastError = (() => {
|
|
330
|
+
for (const r of sliceForError) {
|
|
331
|
+
if (r.status !== "rejected") continue;
|
|
332
|
+
const reason = r.reason;
|
|
333
|
+
if (!(typeof reason === "object" && reason !== null && reason.pipelineAborted === true)) return reason;
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
})() ?? (() => {
|
|
337
|
+
const le = session.lastError;
|
|
338
|
+
if (le === null || le === void 0) return null;
|
|
339
|
+
if (typeof le === "object" && le.pipelineAborted) return null;
|
|
340
|
+
return le;
|
|
341
|
+
})();
|
|
342
|
+
for (let i = alreadyDrained; i < entries.length; i++) {
|
|
343
|
+
const r = entries[i];
|
|
344
|
+
if (r.status === "fulfilled") {
|
|
345
|
+
const rs = r.value;
|
|
346
|
+
for (const n of rs.notices) {
|
|
347
|
+
let out = `${n.severity}: ${n.message}\n`;
|
|
348
|
+
if (n.detail !== void 0) out += `DETAIL: ${n.detail}\n`;
|
|
349
|
+
if (n.hint !== void 0) out += `HINT: ${n.hint}\n`;
|
|
350
|
+
writeErr(out);
|
|
351
|
+
}
|
|
352
|
+
const isSyncOrPlaceholder = rs.fields.length === 0 && rs.command === "" && rs.rows.length === 0;
|
|
353
|
+
const isCommandOnly = rs.fields.length === 0 && rs.rows.length === 0 && rs.command !== "";
|
|
354
|
+
if (!isSyncOrPlaceholder && !isCommandOnly) if (rs.fields.length === 0 && rs.rows.length > 0) {
|
|
355
|
+
if (!ctx.settings.popt.topt.tuplesOnly) {
|
|
356
|
+
process.stdout.write("--\n");
|
|
357
|
+
process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
|
|
358
|
+
}
|
|
359
|
+
} else await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
|
|
360
|
+
} else if (!errorRendered) {
|
|
361
|
+
const reason = r.reason;
|
|
362
|
+
const isAborted = typeof reason === "object" && reason !== null && reason.pipelineAborted === true;
|
|
363
|
+
if (fetchCountActive) writeErr("fetching results in chunked mode failed\n");
|
|
364
|
+
if (isAborted && realLastError !== null) renderPipelineError(realLastError);
|
|
365
|
+
else if (isAborted) writeErr("Pipeline aborted, command did not run\n");
|
|
366
|
+
else renderPipelineError(reason);
|
|
367
|
+
errorRendered = true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (!errorRendered) {
|
|
371
|
+
const lastErr = session.lastError;
|
|
372
|
+
if (lastErr !== null && lastErr !== void 0) {
|
|
373
|
+
if (fetchCountActive) writeErr("fetching results in chunked mode failed\n");
|
|
374
|
+
renderPipelineError(lastErr);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { status: "ok" };
|
|
378
|
+
} catch (err) {
|
|
379
|
+
stashOf(ctx.settings)[PIPELINE_KEY] = void 0;
|
|
380
|
+
ctx.settings.sendMode = "extended-query";
|
|
381
|
+
resetPipelineCounters(ctx.settings);
|
|
382
|
+
clearPipelineGateErrors(ctx.settings);
|
|
383
|
+
return errResult(ctx, errorToMessage(err));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
722
386
|
};
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
// server, and Sync is itself counted as a piped command boundary.
|
|
741
|
-
const queued = readCounter(ctx.settings, "PIPELINE_COMMAND_COUNT");
|
|
742
|
-
setCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 0);
|
|
743
|
-
bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", queued);
|
|
744
|
-
bumpCounter(ctx.settings, "PIPELINE_SYNC_COUNT", 1);
|
|
745
|
-
return { status: "ok" };
|
|
746
|
-
}
|
|
747
|
-
catch (err) {
|
|
748
|
-
return errResult(ctx, errorToMessage(err));
|
|
749
|
-
}
|
|
750
|
-
},
|
|
387
|
+
const cmdSyncPipeline = {
|
|
388
|
+
name: "syncpipeline",
|
|
389
|
+
helpKey: "syncpipeline",
|
|
390
|
+
async run(ctx) {
|
|
391
|
+
const ps = getPipelineState(ctx.settings);
|
|
392
|
+
if (!ps) return errResult(ctx, "no pipeline active");
|
|
393
|
+
try {
|
|
394
|
+
await ps.session.sync();
|
|
395
|
+
const queued = readCounter(ctx.settings, "PIPELINE_COMMAND_COUNT");
|
|
396
|
+
setCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 0);
|
|
397
|
+
bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", queued);
|
|
398
|
+
bumpCounter(ctx.settings, "PIPELINE_SYNC_COUNT", 1);
|
|
399
|
+
return { status: "ok" };
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return errResult(ctx, errorToMessage(err));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
751
404
|
};
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return { status: "ok" };
|
|
769
|
-
}
|
|
770
|
-
catch (err) {
|
|
771
|
-
return errResult(ctx, errorToMessage(err));
|
|
772
|
-
}
|
|
773
|
-
},
|
|
405
|
+
const cmdFlushRequest = {
|
|
406
|
+
name: "flushrequest",
|
|
407
|
+
helpKey: "flushrequest",
|
|
408
|
+
async run(ctx) {
|
|
409
|
+
const ps = getPipelineState(ctx.settings);
|
|
410
|
+
if (!ps) return errResult(ctx, "no pipeline active");
|
|
411
|
+
try {
|
|
412
|
+
await ps.session.flush();
|
|
413
|
+
const queued = readCounter(ctx.settings, "PIPELINE_COMMAND_COUNT");
|
|
414
|
+
setCounter(ctx.settings, "PIPELINE_COMMAND_COUNT", 0);
|
|
415
|
+
bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", queued);
|
|
416
|
+
return { status: "ok" };
|
|
417
|
+
} catch (err) {
|
|
418
|
+
return errResult(ctx, errorToMessage(err));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
774
421
|
};
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
422
|
+
const cmdFlush = {
|
|
423
|
+
name: "flush",
|
|
424
|
+
helpKey: "flush",
|
|
425
|
+
run(ctx) {
|
|
426
|
+
return cmdFlushRequest.run(ctx);
|
|
427
|
+
}
|
|
781
428
|
};
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
try {
|
|
834
|
-
// We send the full P/B/E sequence without an intervening Sync — the
|
|
835
|
-
// user is expected to call \syncpipeline or \endpipeline to commit.
|
|
836
|
-
// For `\bind_named`, skip Parse (the prep stmt already exists on
|
|
837
|
-
// the server, named by the user). For anonymous `\bind`, queue
|
|
838
|
-
// an unnamed Parse so the SQL is parsed on the server in this
|
|
839
|
-
// batch.
|
|
840
|
-
if (!bind.byName) {
|
|
841
|
-
await ps.session.parse("", sql, []);
|
|
842
|
-
}
|
|
843
|
-
await ps.session.bind(stmtName, params);
|
|
844
|
-
const exec = (async () => {
|
|
845
|
-
await ps.session.execute("", 0);
|
|
846
|
-
// PipelineSession.execute resolves with void on the public API; the
|
|
847
|
-
// session internally tracks the ResultSet and surfaces it in end().
|
|
848
|
-
return {
|
|
849
|
-
command: "",
|
|
850
|
-
rowCount: null,
|
|
851
|
-
oid: null,
|
|
852
|
-
fields: [],
|
|
853
|
-
rows: [],
|
|
854
|
-
notices: [],
|
|
855
|
-
};
|
|
856
|
-
})();
|
|
857
|
-
ps.pending.push(exec);
|
|
858
|
-
return { status: "reset-buf", newBuf: "" };
|
|
859
|
-
}
|
|
860
|
-
catch (err) {
|
|
861
|
-
return errResult(ctx, errorToMessage(err));
|
|
862
|
-
}
|
|
863
|
-
},
|
|
429
|
+
const cmdSendPipeline = {
|
|
430
|
+
name: "sendpipeline",
|
|
431
|
+
helpKey: "sendpipeline",
|
|
432
|
+
async run(ctx) {
|
|
433
|
+
const ps = getPipelineState(ctx.settings);
|
|
434
|
+
if (!ps) {
|
|
435
|
+
consumeBindState(ctx.settings);
|
|
436
|
+
ctx.settings.lastErrorResult = { message: "\\sendpipeline not allowed outside of pipeline mode" };
|
|
437
|
+
writeErr("\\sendpipeline not allowed outside of pipeline mode\n");
|
|
438
|
+
return {
|
|
439
|
+
status: "error",
|
|
440
|
+
errorWritten: true
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const bind = consumeBindState(ctx.settings);
|
|
444
|
+
if (bind === null) {
|
|
445
|
+
ctx.settings.lastErrorResult = { message: "\\sendpipeline must be used after \\bind or \\bind_named" };
|
|
446
|
+
writeErr("\\sendpipeline must be used after \\bind or \\bind_named\n");
|
|
447
|
+
return {
|
|
448
|
+
status: "error",
|
|
449
|
+
errorWritten: true
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const sql = ctx.queryBuf.trim();
|
|
453
|
+
const stmtName = bind.name;
|
|
454
|
+
const params = bind.values;
|
|
455
|
+
if (!bind.byName && sql.length === 0) return errResult(ctx, "no query buffer");
|
|
456
|
+
try {
|
|
457
|
+
if (!bind.byName) await ps.session.parse("", sql, []);
|
|
458
|
+
await ps.session.bind(stmtName, params);
|
|
459
|
+
const exec = (async () => {
|
|
460
|
+
await ps.session.execute("", 0);
|
|
461
|
+
return {
|
|
462
|
+
command: "",
|
|
463
|
+
rowCount: null,
|
|
464
|
+
oid: null,
|
|
465
|
+
fields: [],
|
|
466
|
+
rows: [],
|
|
467
|
+
notices: []
|
|
468
|
+
};
|
|
469
|
+
})();
|
|
470
|
+
ps.pending.push(exec);
|
|
471
|
+
return {
|
|
472
|
+
status: "reset-buf",
|
|
473
|
+
newBuf: ""
|
|
474
|
+
};
|
|
475
|
+
} catch (err) {
|
|
476
|
+
return errResult(ctx, errorToMessage(err));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
864
479
|
};
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
reason !== null &&
|
|
967
|
-
reason
|
|
968
|
-
.pipelineAborted === true;
|
|
969
|
-
if (!isAborted)
|
|
970
|
-
return reason;
|
|
971
|
-
}
|
|
972
|
-
return null;
|
|
973
|
-
})();
|
|
974
|
-
const sessPeek = session;
|
|
975
|
-
const realErr = sliceErr ??
|
|
976
|
-
(typeof sessPeek.peekRealError === "function"
|
|
977
|
-
? await sessPeek.peekRealError()
|
|
978
|
-
: null);
|
|
979
|
-
// Per upstream `\getresults`: emit AT MOST ONE error / aborted
|
|
980
|
-
// line per call, even when the slice contains multiple rejections.
|
|
981
|
-
// First rejection's line wins; subsequent ones are suppressed
|
|
982
|
-
// inline (still implicitly accounted via the counter decrement
|
|
983
|
-
// below). Real ERROR trumps the synthetic `Pipeline aborted, …`
|
|
984
|
-
// marker — see `peekRealError`'s discovery semantics.
|
|
985
|
-
let errorRenderedHere = false;
|
|
986
|
-
let walkedItems = 0;
|
|
987
|
-
let syncsDrained = 0;
|
|
988
|
-
let resultsDrained = 0;
|
|
989
|
-
// Cursor into `slots`: indices ≥ this represent SyncMarkers
|
|
990
|
-
// pushed by `session.sync()`. We can't tag the slot in flight
|
|
991
|
-
// without changing the public shape; instead we attribute the
|
|
992
|
-
// first `syncAvailable` silent placeholders to SYNC_COUNT and
|
|
993
|
-
// the remainder to RESULT_COUNT post-walk.
|
|
994
|
-
for (const r of settled) {
|
|
995
|
-
walkedItems++;
|
|
996
|
-
if (r.status !== "fulfilled") {
|
|
997
|
-
// Rejected promise — Parse / Bind / Execute / Close that the
|
|
998
|
-
// server responded to with ErrorResponse, or a cascaded
|
|
999
|
-
// pipelineAborted marker. Upstream `\getresults` walks
|
|
1000
|
-
// libpq's per-Sync result queue inline: the failed entry
|
|
1001
|
-
// produces an `ERROR: …` (or `Pipeline aborted, …`) on
|
|
1002
|
-
// stderr at the `\getresults` line, not deferred to
|
|
1003
|
-
// `\endpipeline`. Match that — but only render the FIRST
|
|
1004
|
-
// rejection in this call so we don't double-print when an
|
|
1005
|
-
// aborted pipeline funnels multiple sticky rejections
|
|
1006
|
-
// through the same `\getresults`.
|
|
1007
|
-
resultsDrained++;
|
|
1008
|
-
if (!errorRenderedHere) {
|
|
1009
|
-
const reason = r.reason;
|
|
1010
|
-
const isAborted = typeof reason === "object" &&
|
|
1011
|
-
reason !== null &&
|
|
1012
|
-
reason
|
|
1013
|
-
.pipelineAborted === true;
|
|
1014
|
-
if (isAborted && realErr !== null) {
|
|
1015
|
-
renderPipelineError(realErr);
|
|
1016
|
-
}
|
|
1017
|
-
else {
|
|
1018
|
-
renderPipelineError(reason);
|
|
1019
|
-
}
|
|
1020
|
-
errorRenderedHere = true;
|
|
1021
|
-
}
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
const rs = r.value;
|
|
1025
|
-
// Emit any NoticeResponse messages attached to this result to
|
|
1026
|
-
// stderr (libpq shape). Notices arrive interleaved with the
|
|
1027
|
-
// Bind / Execute replies and stick to the relevant ResultSet;
|
|
1028
|
-
// upstream psql renders them inline with each result's prelude.
|
|
1029
|
-
for (const n of rs.notices) {
|
|
1030
|
-
let out = `${n.severity}: ${n.message}\n`;
|
|
1031
|
-
if (n.detail !== undefined)
|
|
1032
|
-
out += `DETAIL: ${n.detail}\n`;
|
|
1033
|
-
if (n.hint !== undefined)
|
|
1034
|
-
out += `HINT: ${n.hint}\n`;
|
|
1035
|
-
writeErr(out);
|
|
1036
|
-
}
|
|
1037
|
-
// Silent placeholder: empty fields, empty command, no rows.
|
|
1038
|
-
// Either a SyncMarker (from `session.sync()`) or a successful
|
|
1039
|
-
// Parse-only / Close-only slot. Both print nothing; counter
|
|
1040
|
-
// attribution is fixed up at the tail of this function based
|
|
1041
|
-
// on `syncAvailable`.
|
|
1042
|
-
if (rs.fields.length === 0 &&
|
|
1043
|
-
rs.command === "" &&
|
|
1044
|
-
rs.rows.length === 0) {
|
|
1045
|
-
syncsDrained++;
|
|
1046
|
-
continue;
|
|
1047
|
-
}
|
|
1048
|
-
resultsDrained++;
|
|
1049
|
-
if (rs.fields.length === 0 && rs.rows.length > 0) {
|
|
1050
|
-
// 0-column tuples result — same upstream placeholder shape as
|
|
1051
|
-
// `\endpipeline` (see comment there).
|
|
1052
|
-
const tuplesOnly = ctx.settings.popt.topt.tuplesOnly;
|
|
1053
|
-
if (!tuplesOnly) {
|
|
1054
|
-
process.stdout.write("--\n");
|
|
1055
|
-
process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
else if (rs.fields.length > 0) {
|
|
1059
|
-
await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
// Tell the wire layer how far the cmd layer has consumed from its
|
|
1063
|
-
// results queue. `PipelineSession.end()` uses this offset to skip
|
|
1064
|
-
// entries that `\getresults` has already inspected; otherwise the
|
|
1065
|
-
// rejected promise we just rendered here would get re-stashed on
|
|
1066
|
-
// `session.lastError` and `\endpipeline`'s fallback would
|
|
1067
|
-
// double-print the same `ERROR: …` line. The offset is into
|
|
1068
|
-
// `results` (the per-Execute promise list) not `cmdSlots`; we
|
|
1069
|
-
// approximate by passing `end` (close enough for the
|
|
1070
|
-
// `_externalDrained` check, which is only consulted by `end()`
|
|
1071
|
-
// to skip ALREADY-INSPECTED rejections).
|
|
1072
|
-
const sessMark = session;
|
|
1073
|
-
if (typeof sessMark.markDrained === "function") {
|
|
1074
|
-
sessMark.markDrained(end);
|
|
1075
|
-
}
|
|
1076
|
-
// Once we surface a rejection inline here, also clear any sticky
|
|
1077
|
-
// `lastError` already stashed by a previous wire-layer scan so
|
|
1078
|
-
// `\endpipeline`'s fallback doesn't re-emit the same diagnostic.
|
|
1079
|
-
if (errorRenderedHere) {
|
|
1080
|
-
const sessAny = session;
|
|
1081
|
-
if (typeof sessAny.clearLastError === "function") {
|
|
1082
|
-
sessAny.clearLastError();
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
// Fallback when `cmdSlots` wasn't populated (test mocks):
|
|
1086
|
-
// decrement RESULT_COUNT first, then SYNC_COUNT. Real
|
|
1087
|
-
// PipelineSession populates `cmdSlots` so the walk above already
|
|
1088
|
-
// attributed each drain to the right counter.
|
|
1089
|
-
if (walkedItems === 0) {
|
|
1090
|
-
const ddata = Math.min(n, resultAvailable);
|
|
1091
|
-
resultsDrained = ddata;
|
|
1092
|
-
syncsDrained = n - ddata;
|
|
1093
|
-
}
|
|
1094
|
-
else {
|
|
1095
|
-
// The walk lumped successful silent placeholders (Parse / Close
|
|
1096
|
-
// OK) into `syncsDrained`. Re-attribute: SYNC_COUNT only goes
|
|
1097
|
-
// down by the number of SyncMarkers we actually drained (capped
|
|
1098
|
-
// at `syncAvailable`); the rest are result-style drains.
|
|
1099
|
-
const actualSyncs = Math.min(syncsDrained, syncAvailable);
|
|
1100
|
-
const extraResults = syncsDrained - actualSyncs;
|
|
1101
|
-
syncsDrained = actualSyncs;
|
|
1102
|
-
resultsDrained += extraResults;
|
|
1103
|
-
}
|
|
1104
|
-
// Decrement counters. Upstream `exec_command_getresults` does the
|
|
1105
|
-
// same accounting: PIPELINE_SYNC_COUNT and PIPELINE_RESULT_COUNT
|
|
1106
|
-
// are decremented by the actually-consumed items in each
|
|
1107
|
-
// category. SYNC_COUNT only goes down when a drain walks an
|
|
1108
|
-
// actual SyncMarker slot — we don't force-reset it when
|
|
1109
|
-
// RESULT_COUNT hits zero, because the queue may still hold the
|
|
1110
|
-
// pending PGRES_PIPELINE_SYNC entry. The regress test
|
|
1111
|
-
// `\getresults 1` x5 after 4 commands + 1 sync drains the 5th
|
|
1112
|
-
// call silently against the SyncMarker; a 6th call would emit
|
|
1113
|
-
// "No pending results to get".
|
|
1114
|
-
bumpCounter(ctx.settings, "PIPELINE_SYNC_COUNT", -syncsDrained);
|
|
1115
|
-
bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", -resultsDrained);
|
|
1116
|
-
return { status: "ok" };
|
|
1117
|
-
}
|
|
1118
|
-
catch (err) {
|
|
1119
|
-
return errResult(ctx, errorToMessage(err));
|
|
1120
|
-
}
|
|
1121
|
-
},
|
|
480
|
+
const cmdGetResults = {
|
|
481
|
+
name: "getresults",
|
|
482
|
+
helpKey: "getresults",
|
|
483
|
+
async run(ctx) {
|
|
484
|
+
const ps = getPipelineState(ctx.settings);
|
|
485
|
+
const arg = ctx.nextArg("normal");
|
|
486
|
+
let requested = null;
|
|
487
|
+
if (arg !== null && arg.length > 0) {
|
|
488
|
+
const parsed = parseInt(arg, 10);
|
|
489
|
+
if (!Number.isFinite(parsed) || parsed < 0) return errResult(ctx, "invalid number of requested results");
|
|
490
|
+
requested = parsed;
|
|
491
|
+
}
|
|
492
|
+
if (!ps) {
|
|
493
|
+
process.stdout.write("No pending results to get\n");
|
|
494
|
+
return { status: "ok" };
|
|
495
|
+
}
|
|
496
|
+
const syncAvailable = readCounter(ctx.settings, "PIPELINE_SYNC_COUNT");
|
|
497
|
+
const resultAvailable = readCounter(ctx.settings, "PIPELINE_RESULT_COUNT");
|
|
498
|
+
const available = syncAvailable + resultAvailable;
|
|
499
|
+
if (available === 0) {
|
|
500
|
+
process.stdout.write("No pending results to get\n");
|
|
501
|
+
return { status: "ok" };
|
|
502
|
+
}
|
|
503
|
+
const n = requested === null || requested === 0 ? available : Math.min(requested, available);
|
|
504
|
+
const session = ps.session;
|
|
505
|
+
const slots = Array.isArray(session.cmdSlots) ? session.cmdSlots : [];
|
|
506
|
+
const start = ps.drainedCount;
|
|
507
|
+
const end = start + n;
|
|
508
|
+
const slice = slots.slice(start, end);
|
|
509
|
+
ps.drainedCount = end;
|
|
510
|
+
ps.pending.splice(0, n);
|
|
511
|
+
try {
|
|
512
|
+
const settled = await Promise.allSettled(slice);
|
|
513
|
+
const sliceErr = (() => {
|
|
514
|
+
for (const r of settled) {
|
|
515
|
+
if (r.status !== "rejected") continue;
|
|
516
|
+
const reason = r.reason;
|
|
517
|
+
if (!(typeof reason === "object" && reason !== null && reason.pipelineAborted === true)) return reason;
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
})();
|
|
521
|
+
const sessPeek = session;
|
|
522
|
+
const realErr = sliceErr ?? (typeof sessPeek.peekRealError === "function" ? await sessPeek.peekRealError() : null);
|
|
523
|
+
let errorRenderedHere = false;
|
|
524
|
+
let walkedItems = 0;
|
|
525
|
+
let syncsDrained = 0;
|
|
526
|
+
let resultsDrained = 0;
|
|
527
|
+
for (const r of settled) {
|
|
528
|
+
walkedItems++;
|
|
529
|
+
if (r.status !== "fulfilled") {
|
|
530
|
+
resultsDrained++;
|
|
531
|
+
if (!errorRenderedHere) {
|
|
532
|
+
const reason = r.reason;
|
|
533
|
+
if (typeof reason === "object" && reason !== null && reason.pipelineAborted === true && realErr !== null) renderPipelineError(realErr);
|
|
534
|
+
else renderPipelineError(reason);
|
|
535
|
+
errorRenderedHere = true;
|
|
536
|
+
}
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const rs = r.value;
|
|
540
|
+
for (const n of rs.notices) {
|
|
541
|
+
let out = `${n.severity}: ${n.message}\n`;
|
|
542
|
+
if (n.detail !== void 0) out += `DETAIL: ${n.detail}\n`;
|
|
543
|
+
if (n.hint !== void 0) out += `HINT: ${n.hint}\n`;
|
|
544
|
+
writeErr(out);
|
|
545
|
+
}
|
|
546
|
+
if (rs.fields.length === 0 && rs.command === "" && rs.rows.length === 0) {
|
|
547
|
+
syncsDrained++;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
resultsDrained++;
|
|
551
|
+
if (rs.fields.length === 0 && rs.rows.length > 0) {
|
|
552
|
+
if (!ctx.settings.popt.topt.tuplesOnly) {
|
|
553
|
+
process.stdout.write("--\n");
|
|
554
|
+
process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
|
|
555
|
+
}
|
|
556
|
+
} else if (rs.fields.length > 0) await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
|
|
557
|
+
}
|
|
558
|
+
const sessMark = session;
|
|
559
|
+
if (typeof sessMark.markDrained === "function") sessMark.markDrained(end);
|
|
560
|
+
if (errorRenderedHere) {
|
|
561
|
+
const sessAny = session;
|
|
562
|
+
if (typeof sessAny.clearLastError === "function") sessAny.clearLastError();
|
|
563
|
+
}
|
|
564
|
+
if (walkedItems === 0) {
|
|
565
|
+
const ddata = Math.min(n, resultAvailable);
|
|
566
|
+
resultsDrained = ddata;
|
|
567
|
+
syncsDrained = n - ddata;
|
|
568
|
+
} else {
|
|
569
|
+
const actualSyncs = Math.min(syncsDrained, syncAvailable);
|
|
570
|
+
const extraResults = syncsDrained - actualSyncs;
|
|
571
|
+
syncsDrained = actualSyncs;
|
|
572
|
+
resultsDrained += extraResults;
|
|
573
|
+
}
|
|
574
|
+
bumpCounter(ctx.settings, "PIPELINE_SYNC_COUNT", -syncsDrained);
|
|
575
|
+
bumpCounter(ctx.settings, "PIPELINE_RESULT_COUNT", -resultsDrained);
|
|
576
|
+
return { status: "ok" };
|
|
577
|
+
} catch (err) {
|
|
578
|
+
return errResult(ctx, errorToMessage(err));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
1122
581
|
};
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
// ---------------------------------------------------------------------------
|
|
1136
|
-
export const registerPipelineCommands = (registry) => {
|
|
1137
|
-
registry.register(cmdBind);
|
|
1138
|
-
registry.register(cmdBindNamed);
|
|
1139
|
-
registry.register(cmdParse);
|
|
1140
|
-
registry.register(cmdClosePrepared);
|
|
1141
|
-
registry.register(cmdStartPipeline);
|
|
1142
|
-
registry.register(cmdEndPipeline);
|
|
1143
|
-
registry.register(cmdSyncPipeline);
|
|
1144
|
-
registry.register(cmdFlushRequest);
|
|
1145
|
-
registry.register(cmdFlush);
|
|
1146
|
-
registry.register(cmdSendPipeline);
|
|
1147
|
-
registry.register(cmdGetResults);
|
|
582
|
+
const registerPipelineCommands = (registry) => {
|
|
583
|
+
registry.register(cmdBind);
|
|
584
|
+
registry.register(cmdBindNamed);
|
|
585
|
+
registry.register(cmdParse);
|
|
586
|
+
registry.register(cmdClosePrepared);
|
|
587
|
+
registry.register(cmdStartPipeline);
|
|
588
|
+
registry.register(cmdEndPipeline);
|
|
589
|
+
registry.register(cmdSyncPipeline);
|
|
590
|
+
registry.register(cmdFlushRequest);
|
|
591
|
+
registry.register(cmdFlush);
|
|
592
|
+
registry.register(cmdSendPipeline);
|
|
593
|
+
registry.register(cmdGetResults);
|
|
1148
594
|
};
|
|
595
|
+
//#endregion
|
|
596
|
+
export { cmdBind, cmdBindNamed, cmdClosePrepared, cmdEndPipeline, cmdFlush, cmdFlushRequest, cmdGdesc, cmdGetResults, cmdParse, cmdSendPipeline, cmdStartPipeline, cmdSyncPipeline, consumeBindState, dropPrepared, getPipelineState, lookupPrepared, registerPipelineCommands, stagedNamedBindPresent, stashPrepared };
|