neon 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +354 -517
- package/dist/_shared/env-core/reuse-secrets.js +159 -203
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +573 -690
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -355
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -68
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +130 -189
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +7 -5
package/dist/psql/core/common.js
CHANGED
|
@@ -1,1253 +1,835 @@
|
|
|
1
|
-
|
|
2
|
-
* psql common — the unified send-query / process-result pipeline.
|
|
3
|
-
*
|
|
4
|
-
* TypeScript port of selected functions in `src/bin/psql/common.c`:
|
|
5
|
-
*
|
|
6
|
-
* - {@link sendQuery} ← `SendQuery`
|
|
7
|
-
* - {@link executeAndPrint} ← `ExecQueryAndProcessResults` (the inner
|
|
8
|
-
* result-processing slice, without the
|
|
9
|
-
* AUTOCOMMIT / savepoint scaffolding)
|
|
10
|
-
* - {@link psqlExec} ← `PSQLexec`
|
|
11
|
-
*
|
|
12
|
-
* The minimal version of this logic was inlined in `mainloop.ts` after WP-12;
|
|
13
|
-
* this WP extracts and polishes it. The pieces wired in here:
|
|
14
|
-
*
|
|
15
|
-
* - AUTOCOMMIT: when the variable is 'off' (default 'on') and the current
|
|
16
|
-
* transaction is idle, prepend `BEGIN` once before the next non-exempt
|
|
17
|
-
* statement. The set of exempt commands mirrors upstream
|
|
18
|
-
* `command_no_begin()` — transaction-control verbs and a handful of
|
|
19
|
-
* non-transactional DDL.
|
|
20
|
-
*
|
|
21
|
-
* - ON_ERROR_ROLLBACK: 'off' (default) | 'on' | 'interactive'. When active
|
|
22
|
-
* and we're inside a transaction, issue
|
|
23
|
-
* `SAVEPOINT pg_psql_temporary_savepoint` before the statement; on error
|
|
24
|
-
* `ROLLBACK TO`, on success `RELEASE` (unless the statement is itself a
|
|
25
|
-
* savepoint-management verb that already drops/replaces it).
|
|
26
|
-
*
|
|
27
|
-
* - FETCH_COUNT: integer; when >0 and the statement is a single SELECT-ish
|
|
28
|
-
* query, wrap it in `DECLARE _psql_cursor CURSOR FOR ...; FETCH FORWARD
|
|
29
|
-
* N FROM _psql_cursor` until exhausted. Non-SELECT statements fall back
|
|
30
|
-
* to the simple path.
|
|
31
|
-
*
|
|
32
|
-
* - SINGLESTEP: when `settings.singlestep` is true, print the SQL to stderr
|
|
33
|
-
* with the upstream confirmation banner and read one line from stdin.
|
|
34
|
-
* Input that starts with 'x' cancels the statement.
|
|
35
|
-
*
|
|
36
|
-
* - Timing: when `settings.timing` is true, measure wallclock around the
|
|
37
|
-
* send/print path and write a `Time: X.XXX ms` line to stdout (mirrors
|
|
38
|
-
* upstream's `printf` in common.c, and matches the existing mainloop
|
|
39
|
-
* test expectations).
|
|
40
|
-
*
|
|
41
|
-
* What's deliberately not done here:
|
|
42
|
-
*
|
|
43
|
-
* - SINGLELINE (-S): treating LF as a semicolon is a scanner concern, so it
|
|
44
|
-
* lives there — `scanSql` honours `ScanOptions.singleline` and the mainloop
|
|
45
|
-
* passes `settings.singleline` through on each pass. `sendQuery` itself
|
|
46
|
-
* needs no SINGLELINE branch; by the time a statement reaches here the
|
|
47
|
-
* scanner has already drawn the boundary.
|
|
48
|
-
*
|
|
49
|
-
* - Pipeline mode (-X / `\startpipeline`): upstream gates SendQuery on
|
|
50
|
-
* `pset.send_mode == PSQL_SEND_PIPELINE`; that path is owned by WP-21.
|
|
51
|
-
*
|
|
52
|
-
* - COPY FROM STDIN / TO STDOUT: upstream's ProcessResult dispatches on
|
|
53
|
-
* `PGRES_COPY_IN/OUT`. That belongs to WP-16; here we surface a clear
|
|
54
|
-
* error message if we ever see a copy result come back through
|
|
55
|
-
* `execSimple`.
|
|
56
|
-
*/
|
|
57
|
-
import { getQueryFout } from "../command/cmd_io.js";
|
|
58
|
-
import { formatErrorReport, psqlErrorPrefix } from "../command/cmd_meta.js";
|
|
1
|
+
import { formatDurationMs } from "../print/units.js";
|
|
59
2
|
import { alignedPrinter } from "../print/aligned.js";
|
|
60
3
|
import { asciidocPrinter } from "../print/asciidoc.js";
|
|
61
4
|
import { csvPrinter } from "../print/csv.js";
|
|
62
5
|
import { htmlPrinter } from "../print/html.js";
|
|
63
6
|
import { jsonPrinter } from "../print/json.js";
|
|
64
7
|
import { latexLongtablePrinter, latexPrinter } from "../print/latex.js";
|
|
65
|
-
import { openPager, shouldPage } from "../print/pager.js";
|
|
66
8
|
import { troffMsPrinter } from "../print/troff.js";
|
|
67
9
|
import { unalignedPrinter } from "../print/unaligned.js";
|
|
68
|
-
import {
|
|
10
|
+
import { openPager, shouldPage } from "../print/pager.js";
|
|
11
|
+
import { formatErrorReport, psqlErrorPrefix } from "../command/cmd_meta.js";
|
|
12
|
+
import { getQueryFout } from "../command/cmd_io.js";
|
|
13
|
+
//#region src/psql/core/common.ts
|
|
14
|
+
/**
|
|
15
|
+
* psql common — the unified send-query / process-result pipeline.
|
|
16
|
+
*
|
|
17
|
+
* TypeScript port of selected functions in `src/bin/psql/common.c`:
|
|
18
|
+
*
|
|
19
|
+
* - {@link sendQuery} ← `SendQuery`
|
|
20
|
+
* - {@link executeAndPrint} ← `ExecQueryAndProcessResults` (the inner
|
|
21
|
+
* result-processing slice, without the
|
|
22
|
+
* AUTOCOMMIT / savepoint scaffolding)
|
|
23
|
+
* - {@link psqlExec} ← `PSQLexec`
|
|
24
|
+
*
|
|
25
|
+
* The minimal version of this logic was inlined in `mainloop.ts` after WP-12;
|
|
26
|
+
* this WP extracts and polishes it. The pieces wired in here:
|
|
27
|
+
*
|
|
28
|
+
* - AUTOCOMMIT: when the variable is 'off' (default 'on') and the current
|
|
29
|
+
* transaction is idle, prepend `BEGIN` once before the next non-exempt
|
|
30
|
+
* statement. The set of exempt commands mirrors upstream
|
|
31
|
+
* `command_no_begin()` — transaction-control verbs and a handful of
|
|
32
|
+
* non-transactional DDL.
|
|
33
|
+
*
|
|
34
|
+
* - ON_ERROR_ROLLBACK: 'off' (default) | 'on' | 'interactive'. When active
|
|
35
|
+
* and we're inside a transaction, issue
|
|
36
|
+
* `SAVEPOINT pg_psql_temporary_savepoint` before the statement; on error
|
|
37
|
+
* `ROLLBACK TO`, on success `RELEASE` (unless the statement is itself a
|
|
38
|
+
* savepoint-management verb that already drops/replaces it).
|
|
39
|
+
*
|
|
40
|
+
* - FETCH_COUNT: integer; when >0 and the statement is a single SELECT-ish
|
|
41
|
+
* query, wrap it in `DECLARE _psql_cursor CURSOR FOR ...; FETCH FORWARD
|
|
42
|
+
* N FROM _psql_cursor` until exhausted. Non-SELECT statements fall back
|
|
43
|
+
* to the simple path.
|
|
44
|
+
*
|
|
45
|
+
* - SINGLESTEP: when `settings.singlestep` is true, print the SQL to stderr
|
|
46
|
+
* with the upstream confirmation banner and read one line from stdin.
|
|
47
|
+
* Input that starts with 'x' cancels the statement.
|
|
48
|
+
*
|
|
49
|
+
* - Timing: when `settings.timing` is true, measure wallclock around the
|
|
50
|
+
* send/print path and write a `Time: X.XXX ms` line to stdout (mirrors
|
|
51
|
+
* upstream's `printf` in common.c, and matches the existing mainloop
|
|
52
|
+
* test expectations).
|
|
53
|
+
*
|
|
54
|
+
* What's deliberately not done here:
|
|
55
|
+
*
|
|
56
|
+
* - SINGLELINE (-S): treating LF as a semicolon is a scanner concern, so it
|
|
57
|
+
* lives there — `scanSql` honours `ScanOptions.singleline` and the mainloop
|
|
58
|
+
* passes `settings.singleline` through on each pass. `sendQuery` itself
|
|
59
|
+
* needs no SINGLELINE branch; by the time a statement reaches here the
|
|
60
|
+
* scanner has already drawn the boundary.
|
|
61
|
+
*
|
|
62
|
+
* - Pipeline mode (-X / `\startpipeline`): upstream gates SendQuery on
|
|
63
|
+
* `pset.send_mode == PSQL_SEND_PIPELINE`; that path is owned by WP-21.
|
|
64
|
+
*
|
|
65
|
+
* - COPY FROM STDIN / TO STDOUT: upstream's ProcessResult dispatches on
|
|
66
|
+
* `PGRES_COPY_IN/OUT`. That belongs to WP-16; here we surface a clear
|
|
67
|
+
* error message if we ever see a copy result come back through
|
|
68
|
+
* `execSimple`.
|
|
69
|
+
*/
|
|
69
70
|
const readTxStatus = (conn) => {
|
|
70
|
-
|
|
71
|
-
return status ?? "I";
|
|
71
|
+
return conn.txStatus ?? "I";
|
|
72
72
|
};
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Statement classification.
|
|
75
|
-
//
|
|
76
|
-
// Upstream uses two predicates: `command_no_begin()` decides whether a
|
|
77
|
-
// statement is exempt from AUTOCOMMIT's implicit BEGIN, and
|
|
78
|
-
// `is_select_command()` decides whether FETCH_COUNT chunking applies. We
|
|
79
|
-
// mirror both with a lightweight prefix matcher — the SQL was already
|
|
80
|
-
// normalised by the scanner before reaching us.
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
73
|
const SAVEPOINT_NAME = "pg_psql_temporary_savepoint";
|
|
83
74
|
const CURSOR_NAME = "_psql_cursor";
|
|
84
75
|
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
depth++;
|
|
130
|
-
i += 2;
|
|
131
|
-
}
|
|
132
|
-
else if (sql.charCodeAt(i) === 0x2a &&
|
|
133
|
-
sql.charCodeAt(i + 1) === 0x2f) {
|
|
134
|
-
depth--;
|
|
135
|
-
i += 2;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
i++;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
return i === 0 ? sql : sql.slice(i);
|
|
76
|
+
* Strip leading whitespace and `--` line / slash-star block comments from
|
|
77
|
+
* `sql`. Mirrors what upstream psql's scanner advances past before handing a
|
|
78
|
+
* statement to `PQexec` — the server-reported error `position` is a 1-based
|
|
79
|
+
* offset into THAT trimmed buffer, so the `LINE N:` re-print computed from
|
|
80
|
+
* `count('\n')` in `sql.slice(0, position - 1)` aligns with vanilla output
|
|
81
|
+
* only when the same leading prelude is stripped here too.
|
|
82
|
+
*
|
|
83
|
+
* Block comments support nested depths (PG extension). Embedded comments
|
|
84
|
+
* mid-statement are intentionally NOT stripped — they participate in the
|
|
85
|
+
* line count of the executing statement, same as upstream.
|
|
86
|
+
*
|
|
87
|
+
* Exported for cmd_io / cmd_pipeline so backslash commands that capture or
|
|
88
|
+
* inspect `queryBuf` see the same shape that the wire and `lastQuery`
|
|
89
|
+
* receive.
|
|
90
|
+
*/
|
|
91
|
+
const stripLeadingCommentsAndWS = (sql) => {
|
|
92
|
+
let i = 0;
|
|
93
|
+
const n = sql.length;
|
|
94
|
+
while (i < n) {
|
|
95
|
+
const c = sql.charCodeAt(i);
|
|
96
|
+
if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11) {
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (c === 45 && sql.charCodeAt(i + 1) === 45) {
|
|
101
|
+
i += 2;
|
|
102
|
+
while (i < n && sql.charCodeAt(i) !== 10) i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (c === 47 && sql.charCodeAt(i + 1) === 42) {
|
|
106
|
+
i += 2;
|
|
107
|
+
let depth = 1;
|
|
108
|
+
while (i < n && depth > 0) if (sql.charCodeAt(i) === 47 && sql.charCodeAt(i + 1) === 42) {
|
|
109
|
+
depth++;
|
|
110
|
+
i += 2;
|
|
111
|
+
} else if (sql.charCodeAt(i) === 42 && sql.charCodeAt(i + 1) === 47) {
|
|
112
|
+
depth--;
|
|
113
|
+
i += 2;
|
|
114
|
+
} else i++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
return i === 0 ? sql : sql.slice(i);
|
|
146
120
|
};
|
|
147
121
|
/** Strip leading whitespace and SQL comments, then upper-case for matching. */
|
|
148
122
|
const peekKeywords = (sql, count = 3) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
const tail = sql.slice(i);
|
|
175
|
-
// Tokenise on whitespace + a small set of punctuation that can immediately
|
|
176
|
-
// follow a keyword (semicolon, comma, open-paren).
|
|
177
|
-
const words = tail.split(/[\s,;()]+/u, count + 1);
|
|
178
|
-
return words.slice(0, count).map((w) => w.toUpperCase());
|
|
123
|
+
let i = 0;
|
|
124
|
+
while (i < sql.length) {
|
|
125
|
+
const ch = sql[i];
|
|
126
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (ch === "-" && sql[i + 1] === "-") {
|
|
131
|
+
const nl = sql.indexOf("\n", i + 2);
|
|
132
|
+
if (nl === -1) return [];
|
|
133
|
+
i = nl + 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (ch === "/" && sql[i + 1] === "*") {
|
|
137
|
+
const end = sql.indexOf("*/", i + 2);
|
|
138
|
+
if (end === -1) return [];
|
|
139
|
+
i = end + 2;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
return sql.slice(i).split(/[\s,;()]+/u, count + 1).slice(0, count).map((w) => w.toUpperCase());
|
|
179
145
|
};
|
|
180
146
|
/** Mirror of `command_no_begin()` in psql/common.c. */
|
|
181
147
|
const commandNoBegin = (sql) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return false;
|
|
215
|
-
case "DROP":
|
|
216
|
-
// DROP DATABASE / TABLESPACE / INDEX CONCURRENTLY. (There is no
|
|
217
|
-
// `DROP TABLE … CONCURRENTLY` in PostgreSQL — removed that bogus case.)
|
|
218
|
-
if (w1 === "DATABASE" || w1 === "TABLESPACE")
|
|
219
|
-
return true;
|
|
220
|
-
if (w1 === "INDEX" && w2 === "CONCURRENTLY")
|
|
221
|
-
return true;
|
|
222
|
-
return false;
|
|
223
|
-
case "REINDEX":
|
|
224
|
-
// REINDEX DATABASE / SYSTEM / INDEX CONCURRENTLY / TABLE CONCURRENTLY.
|
|
225
|
-
if (w1 === "DATABASE" || w1 === "SYSTEM")
|
|
226
|
-
return true;
|
|
227
|
-
if (w1 === "INDEX" && w2 === "CONCURRENTLY")
|
|
228
|
-
return true;
|
|
229
|
-
if (w1 === "TABLE" && w2 === "CONCURRENTLY")
|
|
230
|
-
return true;
|
|
231
|
-
return false;
|
|
232
|
-
case "ALTER":
|
|
233
|
-
return w1 === "SYSTEM";
|
|
234
|
-
case "DISCARD":
|
|
235
|
-
return w1 === "ALL";
|
|
236
|
-
default:
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
148
|
+
const [w0, w1, w2, w3] = peekKeywords(sql, 4);
|
|
149
|
+
if (!w0) return false;
|
|
150
|
+
switch (w0) {
|
|
151
|
+
case "ABORT":
|
|
152
|
+
case "BEGIN":
|
|
153
|
+
case "COMMIT":
|
|
154
|
+
case "END":
|
|
155
|
+
case "ROLLBACK":
|
|
156
|
+
case "START":
|
|
157
|
+
case "SAVEPOINT":
|
|
158
|
+
case "RELEASE": return true;
|
|
159
|
+
case "PREPARE": return w1 === "TRANSACTION";
|
|
160
|
+
case "VACUUM": return true;
|
|
161
|
+
case "CLUSTER": return w1 === void 0 || w1 === "";
|
|
162
|
+
case "CREATE":
|
|
163
|
+
if (w1 === "DATABASE" || w1 === "TABLESPACE") return true;
|
|
164
|
+
if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
|
|
165
|
+
if (w1 === "UNIQUE" && w2 === "INDEX" && w3 === "CONCURRENTLY") return true;
|
|
166
|
+
return false;
|
|
167
|
+
case "DROP":
|
|
168
|
+
if (w1 === "DATABASE" || w1 === "TABLESPACE") return true;
|
|
169
|
+
if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
|
|
170
|
+
return false;
|
|
171
|
+
case "REINDEX":
|
|
172
|
+
if (w1 === "DATABASE" || w1 === "SYSTEM") return true;
|
|
173
|
+
if (w1 === "INDEX" && w2 === "CONCURRENTLY") return true;
|
|
174
|
+
if (w1 === "TABLE" && w2 === "CONCURRENTLY") return true;
|
|
175
|
+
return false;
|
|
176
|
+
case "ALTER": return w1 === "SYSTEM";
|
|
177
|
+
case "DISCARD": return w1 === "ALL";
|
|
178
|
+
default: return false;
|
|
179
|
+
}
|
|
239
180
|
};
|
|
240
181
|
/** True when the statement opens with SELECT / VALUES / TABLE / WITH. */
|
|
241
182
|
const isSelectCommand = (sql) => {
|
|
242
|
-
|
|
243
|
-
|
|
183
|
+
const [w0] = peekKeywords(sql, 1);
|
|
184
|
+
return w0 === "SELECT" || w0 === "VALUES" || w0 === "TABLE" || w0 === "WITH";
|
|
244
185
|
};
|
|
245
186
|
/**
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
187
|
+
* Does this statement effectively destroy / replace the temporary savepoint?
|
|
188
|
+
* Upstream's `svpt_gone` flag is set when the user's command is one of
|
|
189
|
+
* COMMIT / ROLLBACK / SAVEPOINT / RELEASE. In those cases we must skip the
|
|
190
|
+
* matching RELEASE because the named savepoint no longer exists.
|
|
191
|
+
*/
|
|
251
192
|
const destroysSavepoint = (sql) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
w0 === "ROLLBACK" ||
|
|
255
|
-
w0 === "SAVEPOINT" ||
|
|
256
|
-
w0 === "RELEASE");
|
|
193
|
+
const [w0] = peekKeywords(sql, 1);
|
|
194
|
+
return w0 === "COMMIT" || w0 === "ROLLBACK" || w0 === "SAVEPOINT" || w0 === "RELEASE";
|
|
257
195
|
};
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
// Printer selection. Routes to the printer for the active output format —
|
|
260
|
-
// every format in {@link OutputFormat} that we ship is wired here; `wrapped`
|
|
261
|
-
// falls back to the aligned printer (which renders `wrapped` mode itself
|
|
262
|
-
// via `topt`).
|
|
263
|
-
// ---------------------------------------------------------------------------
|
|
264
196
|
const pickPrinter = (settings) => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return asciidocPrinter;
|
|
279
|
-
case "latex":
|
|
280
|
-
return latexPrinter;
|
|
281
|
-
case "latex-longtable":
|
|
282
|
-
return latexLongtablePrinter;
|
|
283
|
-
case "troff-ms":
|
|
284
|
-
return troffMsPrinter;
|
|
285
|
-
default:
|
|
286
|
-
return alignedPrinter;
|
|
287
|
-
}
|
|
197
|
+
switch (settings.popt.topt.format) {
|
|
198
|
+
case "aligned":
|
|
199
|
+
case "wrapped": return alignedPrinter;
|
|
200
|
+
case "unaligned": return unalignedPrinter;
|
|
201
|
+
case "csv": return csvPrinter;
|
|
202
|
+
case "json": return jsonPrinter;
|
|
203
|
+
case "html": return htmlPrinter;
|
|
204
|
+
case "asciidoc": return asciidocPrinter;
|
|
205
|
+
case "latex": return latexPrinter;
|
|
206
|
+
case "latex-longtable": return latexLongtablePrinter;
|
|
207
|
+
case "troff-ms": return troffMsPrinter;
|
|
208
|
+
default: return alignedPrinter;
|
|
209
|
+
}
|
|
288
210
|
};
|
|
289
211
|
/**
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return getQueryFout(ctx.settings) ?? ctx.stdout;
|
|
212
|
+
* Pick the output target for a query result.
|
|
213
|
+
*
|
|
214
|
+
* Precedence: explicit `oneShot` argument > the settings stash from
|
|
215
|
+
* `\o FILE` (WP-15) > the REPL context's `stdout`.
|
|
216
|
+
*/
|
|
217
|
+
const pickOut = (ctx, oneShot) => {
|
|
218
|
+
if (oneShot) return oneShot;
|
|
219
|
+
return getQueryFout(ctx.settings) ?? ctx.stdout;
|
|
299
220
|
};
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
// Settings accessors.
|
|
302
|
-
//
|
|
303
|
-
// AUTOCOMMIT, ON_ERROR_ROLLBACK, FETCH_COUNT all live in the psql var store.
|
|
304
|
-
// Upstream reads them once at the start of SendQuery; we do the same so a
|
|
305
|
-
// hook that mutates them mid-query doesn't reshape our logic underneath us.
|
|
306
|
-
// ---------------------------------------------------------------------------
|
|
307
221
|
const readAutocommit = (settings) => settings.vars.asBool("AUTOCOMMIT", true);
|
|
308
222
|
const readOnErrorRollback = (settings) => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (v === "on" || v === "true" || v === "yes" || v === "1")
|
|
316
|
-
return "on";
|
|
317
|
-
return "off";
|
|
223
|
+
const raw = settings.vars.get("ON_ERROR_ROLLBACK");
|
|
224
|
+
if (raw === void 0) return settings.onErrorRollback;
|
|
225
|
+
const v = raw.toLowerCase();
|
|
226
|
+
if (v === "interactive") return "interactive";
|
|
227
|
+
if (v === "on" || v === "true" || v === "yes" || v === "1") return "on";
|
|
228
|
+
return "off";
|
|
318
229
|
};
|
|
319
230
|
const readFetchCount = (settings) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return Math.max(0, v | 0);
|
|
231
|
+
const v = settings.vars.asInt("FETCH_COUNT", settings.fetchCount);
|
|
232
|
+
if (typeof v !== "number") return 0;
|
|
233
|
+
return Math.max(0, v | 0);
|
|
324
234
|
};
|
|
325
235
|
const readSinglestep = (settings) => settings.singlestep || settings.vars.asBool("SINGLESTEP", false);
|
|
326
236
|
/**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
237
|
+
* SHOW_ALL_RESULTS controls multi-statement `\;` printing. Default 'on' —
|
|
238
|
+
* every result set is rendered. When 'off' / '0', only the LAST result set
|
|
239
|
+
* is printed (upstream's `pset.show_all_results` flag, consulted by
|
|
240
|
+
* `PrintQueryResults` in common.c).
|
|
241
|
+
*/
|
|
332
242
|
const readShowAllResults = (settings) => settings.vars.asBool("SHOW_ALL_RESULTS", true);
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
// Error printing — mirrors mainloop's `writeError` format. We keep it local
|
|
335
|
-
// so callers other than the mainloop can still emit consistent errors.
|
|
336
|
-
//
|
|
337
|
-
// `writeError` handles client-side diagnostics (e.g., "no connection to the
|
|
338
|
-
// server") that have no server-side ErrorResponse payload — we emit a single
|
|
339
|
-
// `psql: ERROR: <msg>` line.
|
|
340
|
-
//
|
|
341
|
-
// `writeQueryError` is used after a thrown query error has been captured
|
|
342
|
-
// into `settings.lastErrorResult` by `recordError` / `captureLastError`. It
|
|
343
|
-
// dispatches through {@link formatErrorReport} so the verbosity and
|
|
344
|
-
// SHOW_CONTEXT settings decide whether to surface the SQLSTATE / LINE /
|
|
345
|
-
// caret / DETAIL / HINT / CONTEXT / LOCATION layers. Matches upstream
|
|
346
|
-
// psql's `pg_log_error` shape in `src/bin/psql/common.c`.
|
|
347
|
-
// ---------------------------------------------------------------------------
|
|
348
243
|
const writeError = (ctx, message) => {
|
|
349
|
-
|
|
244
|
+
ctx.stderr.write(`psql: ERROR: ${message}\n`);
|
|
350
245
|
};
|
|
351
246
|
/**
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
ctx.stderr.write(prefixed.join("\n") + "\n");
|
|
247
|
+
* Render the verbosity-aware error report for the most recently captured
|
|
248
|
+
* query error and write it to `ctx.stderr`. The leading severity line gets
|
|
249
|
+
* the same `psql:[<file>:<n>]:` diagnostic prefix that upstream's
|
|
250
|
+
* `pg_log_pre_callback` prepends — matching the format the regression-
|
|
251
|
+
* derived conformance suite expects (e.g. `psql:<stdin>:N: ERROR: ...`).
|
|
252
|
+
* Subsequent layers (`LINE N: ...`, caret, `DETAIL: ...`, ...) follow on
|
|
253
|
+
* their own lines per {@link formatErrorReport}, unprefixed, to match
|
|
254
|
+
* libpq's `PQresultErrorMessage` output shape. When the captured error is
|
|
255
|
+
* missing (defensive — callers should always pair this with a preceding
|
|
256
|
+
* `recordError`) we fall back to the plain client-side {@link writeError}
|
|
257
|
+
* form so we never swallow the message entirely.
|
|
258
|
+
*
|
|
259
|
+
* Exported so the bind-path in mainloop can share the renderer.
|
|
260
|
+
*/
|
|
261
|
+
const writeQueryError = (ctx, fallbackMessage) => {
|
|
262
|
+
const e = ctx.settings.lastErrorResult;
|
|
263
|
+
if (!e || !e.message && !e.code && !e.sqlstate) {
|
|
264
|
+
writeError(ctx, fallbackMessage);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const lines = formatErrorReport(e, ctx.settings.verbosity, ctx.settings.showContext);
|
|
268
|
+
const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
|
|
269
|
+
ctx.stderr.write(prefixed.join("\n") + "\n");
|
|
376
270
|
};
|
|
377
271
|
/**
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
272
|
+
* Strip leading whitespace from a query and rebase a 1-based server position
|
|
273
|
+
* to match. Mirrors upstream psql/mainloop.c's behaviour: a line containing
|
|
274
|
+
* only a backslash command does not leave a `\n` in the query buffer, so the
|
|
275
|
+
* subsequent SQL statement starts at "line 1" of its own context rather than
|
|
276
|
+
* inheriting a blank line. Our mainloop doesn't perform that strip, so the
|
|
277
|
+
* captured `sqlText` sometimes has a leading `\n` (e.g. after `\set
|
|
278
|
+
* FETCH_COUNT 1\nSELECT error;`). Without this normalisation, `\errverbose`
|
|
279
|
+
* would render `LINE 2: SELECT error;` where upstream renders `LINE 1: …`.
|
|
280
|
+
*
|
|
281
|
+
* Returns the trimmed text and the rebased position. If the rebased
|
|
282
|
+
* position would land outside the trimmed text, it is dropped so the
|
|
283
|
+
* formatter skips the `LINE`/caret block instead of mis-pointing.
|
|
284
|
+
*/
|
|
391
285
|
const normaliseSqlAndPosition = (sqlText, position) => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
286
|
+
let leading = 0;
|
|
287
|
+
while (leading < sqlText.length) {
|
|
288
|
+
const ch = sqlText.charCodeAt(leading);
|
|
289
|
+
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13 && ch !== 12) break;
|
|
290
|
+
leading++;
|
|
291
|
+
}
|
|
292
|
+
if (leading === 0) return {
|
|
293
|
+
sqlText,
|
|
294
|
+
position
|
|
295
|
+
};
|
|
296
|
+
const trimmed = sqlText.slice(leading);
|
|
297
|
+
if (typeof position !== "string") return {
|
|
298
|
+
sqlText: trimmed,
|
|
299
|
+
position
|
|
300
|
+
};
|
|
301
|
+
const original = parseInt(position, 10);
|
|
302
|
+
if (!Number.isFinite(original) || original <= 0) return {
|
|
303
|
+
sqlText: trimmed,
|
|
304
|
+
position
|
|
305
|
+
};
|
|
306
|
+
const rebased = original - leading;
|
|
307
|
+
if (rebased <= 0 || rebased > trimmed.length) return {
|
|
308
|
+
sqlText: trimmed,
|
|
309
|
+
position: void 0
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
sqlText: trimmed,
|
|
313
|
+
position: String(rebased)
|
|
314
|
+
};
|
|
419
315
|
};
|
|
420
316
|
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
return settings.lastErrorResult.message ?? fallbackMessage;
|
|
317
|
+
* Capture the full ErrorResponse-shaped payload from a thrown error.
|
|
318
|
+
*
|
|
319
|
+
* Our wire layer copies every named field of the server's ErrorResponse
|
|
320
|
+
* (severity / code / detail / hint / position / file / line / routine /
|
|
321
|
+
* …) onto the thrown Error as own properties (see `asThrowable` in
|
|
322
|
+
* `wire/connection.ts`). We mirror those onto `settings.lastErrorResult`
|
|
323
|
+
* so `\errverbose` can re-render the error in VERBOSE form — including
|
|
324
|
+
* the `LINE N: …` re-print + `^` pointer and the `LOCATION:` footer.
|
|
325
|
+
*
|
|
326
|
+
* `sqlText` is the originating SQL text from the caller; required so the
|
|
327
|
+
* `^` pointer can be positioned under the failing character. Leading
|
|
328
|
+
* whitespace is stripped (and `position` is rebased) so the `LINE N`
|
|
329
|
+
* counter reflects offsets within the user's statement rather than any
|
|
330
|
+
* buffer noise carried over from prior backslash commands.
|
|
331
|
+
*/
|
|
332
|
+
const captureLastError = (settings, err, sqlText) => {
|
|
333
|
+
const fallbackMessage = err instanceof Error ? err.message : String(err);
|
|
334
|
+
const e = err ?? {};
|
|
335
|
+
const code = e.code;
|
|
336
|
+
const normalised = normaliseSqlAndPosition(sqlText, e.position);
|
|
337
|
+
settings.lastErrorResult = {
|
|
338
|
+
severity: e.severity,
|
|
339
|
+
code,
|
|
340
|
+
sqlstate: code,
|
|
341
|
+
message: e.message ?? fallbackMessage,
|
|
342
|
+
detail: e.detail,
|
|
343
|
+
hint: e.hint,
|
|
344
|
+
position: normalised.position,
|
|
345
|
+
internalPosition: e.internalPosition,
|
|
346
|
+
internalQuery: e.internalQuery,
|
|
347
|
+
where: e.where,
|
|
348
|
+
schema: e.schema,
|
|
349
|
+
table: e.table,
|
|
350
|
+
column: e.column,
|
|
351
|
+
dataType: e.dataType,
|
|
352
|
+
constraint: e.constraint,
|
|
353
|
+
file: e.file,
|
|
354
|
+
line: e.line,
|
|
355
|
+
routine: e.routine,
|
|
356
|
+
sqlText: normalised.sqlText
|
|
357
|
+
};
|
|
358
|
+
return settings.lastErrorResult.message ?? fallbackMessage;
|
|
464
359
|
};
|
|
465
360
|
const recordError = (ctx, err, sqlText = "") => captureLastError(ctx.settings, err, sqlText);
|
|
466
361
|
/**
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
362
|
+
* Update the per-statement diagnostic psql variables that upstream's
|
|
363
|
+
* `SetResultVariables` / `SetErrorVariables` in `src/bin/psql/common.c`
|
|
364
|
+
* maintains. Called after every dispatched statement (success and error
|
|
365
|
+
* paths) so `\echo :LAST_ERROR_MESSAGE` and friends produce the same
|
|
366
|
+
* values vanilla psql does.
|
|
367
|
+
*
|
|
368
|
+
* - `SQLSTATE` SQLSTATE of the *most recent* statement —
|
|
369
|
+
* `"00000"` on success, the server-reported code
|
|
370
|
+
* on error (defaults to `"XX000"` when missing).
|
|
371
|
+
* - `ERROR` `"true"` if the most recent statement failed,
|
|
372
|
+
* else `"false"`.
|
|
373
|
+
* - `ROW_COUNT` affected/returned row count of the most recent
|
|
374
|
+
* statement (from libpq's `PQcmdTuples`). `"0"`
|
|
375
|
+
* on error.
|
|
376
|
+
* - `LAST_ERROR_*` sticky — only mutated on error. Mirrors
|
|
377
|
+
* upstream's "preserve until the next failure"
|
|
378
|
+
* contract so a successful statement does not
|
|
379
|
+
* clobber the prior error info.
|
|
380
|
+
*
|
|
381
|
+
* Exported so mainloop's bind / pipeline paths (which bypass {@link
|
|
382
|
+
* sendQuery}) can share the same updater.
|
|
383
|
+
*/
|
|
384
|
+
const refreshErrorVars = (settings, outcome) => {
|
|
385
|
+
const { vars } = settings;
|
|
386
|
+
if (outcome.kind === "error") {
|
|
387
|
+
const last = settings.lastErrorResult;
|
|
388
|
+
const code = last?.code ?? last?.sqlstate ?? "XX000";
|
|
389
|
+
const message = last?.message ?? "";
|
|
390
|
+
vars.set("LAST_ERROR_MESSAGE", message);
|
|
391
|
+
vars.set("LAST_ERROR_SQLSTATE", code);
|
|
392
|
+
vars.set("SQLSTATE", code);
|
|
393
|
+
vars.set("ERROR", "true");
|
|
394
|
+
vars.set("ROW_COUNT", "0");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
vars.set("SQLSTATE", "00000");
|
|
398
|
+
vars.set("ERROR", "false");
|
|
399
|
+
const rc = outcome.rowCount ?? 0;
|
|
400
|
+
vars.set("ROW_COUNT", String(rc));
|
|
506
401
|
};
|
|
507
|
-
// ---------------------------------------------------------------------------
|
|
508
|
-
// SINGLESTEP confirmation.
|
|
509
|
-
//
|
|
510
|
-
// Upstream prints to stdout and reads a line from /dev/tty; we use ctx.stderr
|
|
511
|
-
// for the banner (so the SQL preview stays out of any redirected query
|
|
512
|
-
// output) and read one line from ctx.stdin. Returns true to proceed.
|
|
513
|
-
// ---------------------------------------------------------------------------
|
|
514
402
|
const readOneLine = (stdin) => new Promise((resolve) => {
|
|
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
|
-
|
|
403
|
+
let buf = "";
|
|
404
|
+
let resolved = false;
|
|
405
|
+
const onData = (chunk) => {
|
|
406
|
+
buf += chunk.toString();
|
|
407
|
+
const nl = buf.indexOf("\n");
|
|
408
|
+
if (nl !== -1) {
|
|
409
|
+
const line = buf.slice(0, nl);
|
|
410
|
+
cleanup();
|
|
411
|
+
if (!resolved) {
|
|
412
|
+
resolved = true;
|
|
413
|
+
resolve(line);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const onEnd = () => {
|
|
418
|
+
cleanup();
|
|
419
|
+
if (!resolved) {
|
|
420
|
+
resolved = true;
|
|
421
|
+
resolve(buf);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
const cleanup = () => {
|
|
425
|
+
stdin.off("data", onData);
|
|
426
|
+
stdin.off("end", onEnd);
|
|
427
|
+
stdin.off("close", onEnd);
|
|
428
|
+
};
|
|
429
|
+
stdin.on("data", onData);
|
|
430
|
+
stdin.once("end", onEnd);
|
|
431
|
+
stdin.once("close", onEnd);
|
|
544
432
|
});
|
|
545
433
|
const confirmSinglestep = async (ctx, sql) => {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
`***(press return to proceed or enter x and return to cancel)********************\n`);
|
|
549
|
-
const line = await readOneLine(ctx.stdin);
|
|
550
|
-
return !line.trim().toLowerCase().startsWith("x");
|
|
434
|
+
ctx.stderr.write(`***(Single step mode: verify command)*******************************************\n${sql}\n***(press return to proceed or enter x and return to cancel)********************\n`);
|
|
435
|
+
return !(await readOneLine(ctx.stdin)).trim().toLowerCase().startsWith("x");
|
|
551
436
|
};
|
|
552
|
-
// ---------------------------------------------------------------------------
|
|
553
|
-
// Result rendering. We tally rows printed / rows affected for the QueryStats
|
|
554
|
-
// return; both are best-effort against the libpq-shaped ResultSet (rowCount
|
|
555
|
-
// is null for DDL, rows.length is 0 for COPY etc.).
|
|
556
|
-
// ---------------------------------------------------------------------------
|
|
557
437
|
/**
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
438
|
+
* Reconstruct the libpq-style CommandComplete tag from the parsed parts our
|
|
439
|
+
* wire layer stores on a ResultSet. INSERT carries `oid` + `rowCount`; the
|
|
440
|
+
* other DML verbs (UPDATE/DELETE/MERGE/SELECT/MOVE/FETCH/COPY) carry just a
|
|
441
|
+
* `rowCount`; DDL has neither.
|
|
442
|
+
*
|
|
443
|
+
* Matches upstream psql's `PQcmdStatus(conn)` output (which is the raw tag
|
|
444
|
+
* the server sent — we round-trip it through our parser).
|
|
445
|
+
*/
|
|
566
446
|
const formatCommandTag = (rs) => {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
return `INSERT ${rs.oid ?? 0} ${rs.rowCount ?? 0}`;
|
|
573
|
-
}
|
|
574
|
-
if (rs.rowCount !== null && rs.rowCount !== undefined) {
|
|
575
|
-
return `${command} ${rs.rowCount}`;
|
|
576
|
-
}
|
|
577
|
-
return command;
|
|
447
|
+
const command = (rs.command || "").trim();
|
|
448
|
+
if (command.length === 0) return "";
|
|
449
|
+
if (command === "INSERT") return `INSERT ${rs.oid ?? 0} ${rs.rowCount ?? 0}`;
|
|
450
|
+
if (rs.rowCount !== null && rs.rowCount !== void 0) return `${command} ${rs.rowCount}`;
|
|
451
|
+
return command;
|
|
578
452
|
};
|
|
579
453
|
/**
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
454
|
+
* Decide whether the pager should activate for the given batch of result
|
|
455
|
+
* sets. We page when ANY of the sets-that-will-be-printed (i.e. tuples-
|
|
456
|
+
* producing, and not gated out by SHOW_ALL_RESULTS) crosses the threshold.
|
|
457
|
+
* The decision is centralised here so callers can override individual
|
|
458
|
+
* decision inputs (e.g. tests that inject a fake `output`).
|
|
459
|
+
*/
|
|
586
460
|
const pickPagerDecision = (ctx, results, out) => {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
rowCount: rs.rows.length,
|
|
608
|
-
colCount: rs.fields.length,
|
|
609
|
-
output: out,
|
|
610
|
-
redirectedOutput,
|
|
611
|
-
});
|
|
612
|
-
if (decision)
|
|
613
|
-
return true;
|
|
614
|
-
}
|
|
615
|
-
return false;
|
|
461
|
+
const popt = ctx.settings.popt.topt;
|
|
462
|
+
if (popt.pager === "off") return false;
|
|
463
|
+
const redirectedOutput = getQueryFout(ctx.settings) !== null;
|
|
464
|
+
if (redirectedOutput) return false;
|
|
465
|
+
const showAll = readShowAllResults(ctx.settings);
|
|
466
|
+
const lastIdx = results.length - 1;
|
|
467
|
+
for (let i = 0; i < results.length; i++) {
|
|
468
|
+
const rs = results[i];
|
|
469
|
+
if (rs.fields.length === 0) continue;
|
|
470
|
+
if (!(showAll || i === lastIdx)) continue;
|
|
471
|
+
if (shouldPage({
|
|
472
|
+
pager: popt.pager,
|
|
473
|
+
pagerMinLines: popt.pagerMinLines,
|
|
474
|
+
rowCount: rs.rows.length,
|
|
475
|
+
colCount: rs.fields.length,
|
|
476
|
+
output: out,
|
|
477
|
+
redirectedOutput
|
|
478
|
+
})) return true;
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
616
481
|
};
|
|
617
482
|
const renderResultSets = async (ctx, results, out) => {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (rs.fields.length === 0) {
|
|
662
|
-
// Non-tuples-producing commands (INSERT/UPDATE/DELETE/DDL) — emit the
|
|
663
|
-
// CommandComplete tag instead of running the table printer (which
|
|
664
|
-
// would render an empty `(0 rows)` block). Suppressed in tuples-only
|
|
665
|
-
// mode (`\t`) and in `--quiet` mode to match upstream
|
|
666
|
-
// (PSQLexec calls SetResultVariables which only prints the tag
|
|
667
|
-
// when !pset.quiet). Also suppressed when the result represents a
|
|
668
|
-
// COPY-out segment whose bytes we already streamed above —
|
|
669
|
-
// upstream's `handleCopyOut` doesn't emit the `COPY N` tag on the
|
|
670
|
-
// queryFout stream; the tag goes to the status stream which we
|
|
671
|
-
// route through the diagnostic vars rather than stdout.
|
|
672
|
-
if (shouldEmit &&
|
|
673
|
-
!tuplesOnly &&
|
|
674
|
-
!ctx.settings.quiet &&
|
|
675
|
-
!rs.copyOutBytes) {
|
|
676
|
-
const tag = formatCommandTag(rs);
|
|
677
|
-
if (tag.length > 0)
|
|
678
|
-
sink.write(`${tag}\n`);
|
|
679
|
-
}
|
|
680
|
-
// rowCount is the affected-row total when libpq sets it.
|
|
681
|
-
rowsAffected += rs.rowCount ?? 0;
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
if (shouldEmit) {
|
|
685
|
-
await printer.printQuery(rs, ctx.settings.popt, sink);
|
|
686
|
-
}
|
|
687
|
-
rowsPrinted += rs.rows.length;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
finally {
|
|
692
|
-
if (pager?.spawned) {
|
|
693
|
-
// End the pager stdin and wait for it to exit. We swallow errors here:
|
|
694
|
-
// the user may have closed the pager early (SIGPIPE → EPIPE) and our
|
|
695
|
-
// callers should not see that as a query failure.
|
|
696
|
-
try {
|
|
697
|
-
await pager.close();
|
|
698
|
-
}
|
|
699
|
-
catch {
|
|
700
|
-
// ignore
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
// libpq's `PQcmdTuples(lastResult)` semantic: ROW_COUNT mirrors the LAST
|
|
705
|
-
// result set's affected-row count (or returned-row count for tuples-
|
|
706
|
-
// producing commands). For SELECT-shaped results the wire layer doesn't
|
|
707
|
-
// populate rs.rowCount until CommandComplete arrives, but the array shape
|
|
708
|
-
// (`rs.rows.length`) is the authoritative count.
|
|
709
|
-
const lastRowCount = results.length === 0
|
|
710
|
-
? null
|
|
711
|
-
: (() => {
|
|
712
|
-
const rs = results[results.length - 1];
|
|
713
|
-
if (rs.fields.length > 0)
|
|
714
|
-
return rs.rows.length;
|
|
715
|
-
return rs.rowCount ?? null;
|
|
716
|
-
})();
|
|
717
|
-
return { rowsAffected, rowsPrinted, lastRowCount };
|
|
483
|
+
const printer = pickPrinter(ctx.settings);
|
|
484
|
+
let rowsAffected = 0;
|
|
485
|
+
let rowsPrinted = 0;
|
|
486
|
+
const showAll = readShowAllResults(ctx.settings);
|
|
487
|
+
const lastIdx = results.length - 1;
|
|
488
|
+
const tuplesOnly = ctx.settings.popt.topt.tuplesOnly;
|
|
489
|
+
const pager = pickPagerDecision(ctx, results, out) ? openPager({
|
|
490
|
+
pager: ctx.settings.popt.topt.pager,
|
|
491
|
+
pagerMinLines: ctx.settings.popt.topt.pagerMinLines,
|
|
492
|
+
stdout: out
|
|
493
|
+
}) : null;
|
|
494
|
+
const sink = pager?.spawned ? pager.out : out;
|
|
495
|
+
try {
|
|
496
|
+
for (let i = 0; i < results.length; i++) {
|
|
497
|
+
const rs = results[i];
|
|
498
|
+
const shouldEmit = showAll || i === lastIdx;
|
|
499
|
+
if (rs.copyOutBytes && rs.copyOutBytes.length > 0) for (const chunk of rs.copyOutBytes) sink.write(chunk);
|
|
500
|
+
if (rs.fields.length === 0) {
|
|
501
|
+
if (shouldEmit && !tuplesOnly && !ctx.settings.quiet && !rs.copyOutBytes) {
|
|
502
|
+
const tag = formatCommandTag(rs);
|
|
503
|
+
if (tag.length > 0) sink.write(`${tag}\n`);
|
|
504
|
+
}
|
|
505
|
+
rowsAffected += rs.rowCount ?? 0;
|
|
506
|
+
} else {
|
|
507
|
+
if (shouldEmit) await printer.printQuery(rs, ctx.settings.popt, sink);
|
|
508
|
+
rowsPrinted += rs.rows.length;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} finally {
|
|
512
|
+
if (pager?.spawned) try {
|
|
513
|
+
await pager.close();
|
|
514
|
+
} catch {}
|
|
515
|
+
}
|
|
516
|
+
const lastRowCount = results.length === 0 ? null : (() => {
|
|
517
|
+
const rs = results[results.length - 1];
|
|
518
|
+
if (rs.fields.length > 0) return rs.rows.length;
|
|
519
|
+
return rs.rowCount ?? null;
|
|
520
|
+
})();
|
|
521
|
+
return {
|
|
522
|
+
rowsAffected,
|
|
523
|
+
rowsPrinted,
|
|
524
|
+
lastRowCount
|
|
525
|
+
};
|
|
718
526
|
};
|
|
719
527
|
/**
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
528
|
+
* Render a single {@link ResultSet} through the active printer and the
|
|
529
|
+
* configured output target (respecting `\o FILE` redirects). Used by the
|
|
530
|
+
* `\bind` / extended-query path in {@link mainloop.dispatchSendQuery} which
|
|
531
|
+
* comes back with a single result instead of the array shape `execSimple`
|
|
532
|
+
* produces. Returns a tally consistent with {@link renderResultSets}.
|
|
533
|
+
*/
|
|
534
|
+
const renderResultSet = (ctx, rs, out) => {
|
|
535
|
+
return renderResultSets(ctx, [rs], out ?? pickOut(ctx));
|
|
728
536
|
};
|
|
729
|
-
// ---------------------------------------------------------------------------
|
|
730
|
-
// FETCH_COUNT cursor loop.
|
|
731
|
-
//
|
|
732
|
-
// Wrap `<sql>` in DECLARE/FETCH and stream chunks. We open the cursor inside
|
|
733
|
-
// a transaction (upstream relies on the surrounding implicit BEGIN); when
|
|
734
|
-
// AUTOCOMMIT is on and we're idle, we open one here and close it with a
|
|
735
|
-
// COMMIT on the happy path / ROLLBACK on error.
|
|
736
|
-
// ---------------------------------------------------------------------------
|
|
737
537
|
/**
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
538
|
+
* Re-base a server-side `position` field so it points into the user's
|
|
539
|
+
* original SQL rather than the synthetic statement we actually sent.
|
|
540
|
+
*
|
|
541
|
+
* The FETCH_COUNT path sends `DECLARE _psql_cursor NO SCROLL CURSOR FOR
|
|
542
|
+
* <userSql>` for the DECLARE leg and `FETCH FORWARD N FROM _psql_cursor`
|
|
543
|
+
* for each fetch. Server error positions (`P` field) come back in the
|
|
544
|
+
* coordinates of whatever query we sent:
|
|
545
|
+
*
|
|
546
|
+
* - DECLARE-time parser/planner errors carry a position into the DECLARE
|
|
547
|
+
* statement. We subtract the length of the prefix (`DECLARE … FOR `)
|
|
548
|
+
* so the caret lands under the failing token in `userSql`.
|
|
549
|
+
*
|
|
550
|
+
* - FETCH-time runtime errors come from executing the cursor's underlying
|
|
551
|
+
* query (which IS `userSql`). The server reports the position relative
|
|
552
|
+
* to that underlying query, so it's already in `userSql` coordinates
|
|
553
|
+
* and we leave it alone.
|
|
554
|
+
*
|
|
555
|
+
* If we can't rebase a DECLARE-coord position into `userSql` bounds, we
|
|
556
|
+
* strip it rather than render a caret pointing past end-of-line.
|
|
557
|
+
*/
|
|
758
558
|
const rebasePositionForCursor = (err, wrapper, userSql) => {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (offset === -1) {
|
|
774
|
-
// FETCH-leg failures: the wrapper is `FETCH FORWARD …` and the server
|
|
775
|
-
// reports the position relative to the cursor's underlying query
|
|
776
|
-
// (i.e. `userSql`), not the FETCH text. Leave the position alone —
|
|
777
|
-
// assuming it's already in user-sql coordinates is the right call,
|
|
778
|
-
// and if it isn't, the LINE/caret renderer clamps gracefully.
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
const rebased = original - offset;
|
|
782
|
-
if (rebased <= 0 || rebased > userSql.length) {
|
|
783
|
-
// Position points outside the user's SQL — likely the parser blamed
|
|
784
|
-
// something inside the wrapper. Drop the field so the formatter skips
|
|
785
|
-
// the `LINE`/caret block instead of mis-pointing.
|
|
786
|
-
delete e.position;
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
e.position = String(rebased);
|
|
559
|
+
if (!err || typeof err !== "object") return;
|
|
560
|
+
const e = err;
|
|
561
|
+
if (typeof e.position !== "string") return;
|
|
562
|
+
const original = parseInt(e.position, 10);
|
|
563
|
+
if (!Number.isFinite(original) || original <= 0) return;
|
|
564
|
+
const stripped = userSql.replace(/;\s*$/u, "");
|
|
565
|
+
const offset = wrapper.indexOf(stripped);
|
|
566
|
+
if (offset === -1) return;
|
|
567
|
+
const rebased = original - offset;
|
|
568
|
+
if (rebased <= 0 || rebased > userSql.length) {
|
|
569
|
+
delete e.position;
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
e.position = String(rebased);
|
|
790
573
|
};
|
|
791
574
|
const runCursorLoop = async (ctx, sql, fetchCount, out) => {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
await db.execSimple("COMMIT");
|
|
864
|
-
}
|
|
865
|
-
return { rowsAffected, rowsPrinted, lastRowCount: rowsPrinted };
|
|
866
|
-
}
|
|
867
|
-
catch (err) {
|
|
868
|
-
// Flush whatever chunks we successfully fetched before the error so the
|
|
869
|
-
// partial output lands ahead of the ERROR line. Mirrors upstream
|
|
870
|
-
// print_cursor.c: each chunk renders incrementally — when a later FETCH
|
|
871
|
-
// raises (e.g. division by zero on row 16 of a 10-row chunked stream),
|
|
872
|
-
// the first chunk's rows have already been printed. We accumulate into
|
|
873
|
-
// a single merged ResultSet here, so the partial flush is "print the
|
|
874
|
-
// merged buffer once, without the `(N rows)` footer the happy-path
|
|
875
|
-
// emits when the cursor completes cleanly". The footer is suppressed
|
|
876
|
-
// because the table is conceptually incomplete (upstream renders no
|
|
877
|
-
// `(N rows)` for the truncated chunk either).
|
|
878
|
-
if (merged !== null) {
|
|
879
|
-
merged.rowCount = merged.rows.length;
|
|
880
|
-
const partialOpts = {
|
|
881
|
-
...ctx.settings.popt,
|
|
882
|
-
// `stopTable: false` mirrors upstream `print_cursor.c`'s
|
|
883
|
-
// mid-error flush: no `(N rows)` auto-footer, no trailing
|
|
884
|
-
// blank — the ERROR line should land flush against the last
|
|
885
|
-
// data row, not separated by an extra empty line.
|
|
886
|
-
topt: {
|
|
887
|
-
...ctx.settings.popt.topt,
|
|
888
|
-
defaultFooter: false,
|
|
889
|
-
stopTable: false,
|
|
890
|
-
},
|
|
891
|
-
};
|
|
892
|
-
try {
|
|
893
|
-
await printer.printQuery(merged, partialOpts, out);
|
|
894
|
-
}
|
|
895
|
-
catch {
|
|
896
|
-
// ignore — surface the original error
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
// Rebase the server-reported `position` from the synthetic wrapper's
|
|
900
|
-
// coordinates into the user's SQL coordinates in place. Server error
|
|
901
|
-
// positions come back relative to whatever statement we sent (DECLARE
|
|
902
|
-
// `… FOR <user-sql>` or FETCH FORWARD `…`). Without this rewrite, the
|
|
903
|
-
// caller's `recordError(ctx, err, sql)` would stash a position that
|
|
904
|
-
// points past the end of `sql`, and `\errverbose` would render
|
|
905
|
-
// `LINE 1: <user-sql>` with the `^` caret in the wrong column.
|
|
906
|
-
rebasePositionForCursor(err, currentWrapper, sql);
|
|
907
|
-
if (cursorOpen) {
|
|
908
|
-
try {
|
|
909
|
-
await db.execSimple(`CLOSE ${CURSOR_NAME}`);
|
|
910
|
-
}
|
|
911
|
-
catch {
|
|
912
|
-
// ignore — surface the original error
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
if (initiallyIdle) {
|
|
916
|
-
try {
|
|
917
|
-
await db.execSimple("ROLLBACK");
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
// ignore
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
throw err;
|
|
924
|
-
}
|
|
575
|
+
if (!ctx.settings.db) throw new Error("no connection to the server");
|
|
576
|
+
const db = ctx.settings.db;
|
|
577
|
+
const initiallyIdle = readTxStatus(db) === "I";
|
|
578
|
+
if (initiallyIdle) await db.execSimple("BEGIN");
|
|
579
|
+
const stripped = sql.replace(/;\s*$/u, "");
|
|
580
|
+
const declared = `DECLARE ${CURSOR_NAME} NO SCROLL CURSOR FOR ${stripped}`;
|
|
581
|
+
const fetchSql = `FETCH FORWARD ${String(fetchCount)} FROM ${CURSOR_NAME}`;
|
|
582
|
+
const rowsAffected = 0;
|
|
583
|
+
let rowsPrinted = 0;
|
|
584
|
+
let cursorOpen = false;
|
|
585
|
+
let currentWrapper = declared;
|
|
586
|
+
const printer = pickPrinter(ctx.settings);
|
|
587
|
+
let merged = null;
|
|
588
|
+
try {
|
|
589
|
+
await db.execSimple(declared);
|
|
590
|
+
cursorOpen = true;
|
|
591
|
+
while (true) {
|
|
592
|
+
currentWrapper = fetchSql;
|
|
593
|
+
const sets = await db.execSimple(fetchSql);
|
|
594
|
+
if (sets.length === 0) break;
|
|
595
|
+
const rs = sets[sets.length - 1];
|
|
596
|
+
const chunkRows = rs.rows.length;
|
|
597
|
+
if (chunkRows === 0) break;
|
|
598
|
+
if (merged === null) merged = {
|
|
599
|
+
command: rs.command,
|
|
600
|
+
fields: rs.fields,
|
|
601
|
+
rows: rs.rows.slice(),
|
|
602
|
+
rowCount: rs.rowCount,
|
|
603
|
+
oid: rs.oid,
|
|
604
|
+
notices: rs.notices
|
|
605
|
+
};
|
|
606
|
+
else for (const row of rs.rows) merged.rows.push(row);
|
|
607
|
+
rowsPrinted += chunkRows;
|
|
608
|
+
if (chunkRows < fetchCount) break;
|
|
609
|
+
}
|
|
610
|
+
if (merged !== null) {
|
|
611
|
+
merged.rowCount = merged.rows.length;
|
|
612
|
+
await printer.printQuery(merged, ctx.settings.popt, out);
|
|
613
|
+
}
|
|
614
|
+
await db.execSimple(`CLOSE ${CURSOR_NAME}`);
|
|
615
|
+
cursorOpen = false;
|
|
616
|
+
if (initiallyIdle) await db.execSimple("COMMIT");
|
|
617
|
+
return {
|
|
618
|
+
rowsAffected,
|
|
619
|
+
rowsPrinted,
|
|
620
|
+
lastRowCount: rowsPrinted
|
|
621
|
+
};
|
|
622
|
+
} catch (err) {
|
|
623
|
+
if (merged !== null) {
|
|
624
|
+
merged.rowCount = merged.rows.length;
|
|
625
|
+
const partialOpts = {
|
|
626
|
+
...ctx.settings.popt,
|
|
627
|
+
topt: {
|
|
628
|
+
...ctx.settings.popt.topt,
|
|
629
|
+
defaultFooter: false,
|
|
630
|
+
stopTable: false
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
try {
|
|
634
|
+
await printer.printQuery(merged, partialOpts, out);
|
|
635
|
+
} catch {}
|
|
636
|
+
}
|
|
637
|
+
rebasePositionForCursor(err, currentWrapper, sql);
|
|
638
|
+
if (cursorOpen) try {
|
|
639
|
+
await db.execSimple(`CLOSE ${CURSOR_NAME}`);
|
|
640
|
+
} catch {}
|
|
641
|
+
if (initiallyIdle) try {
|
|
642
|
+
await db.execSimple("ROLLBACK");
|
|
643
|
+
} catch {}
|
|
644
|
+
throw err;
|
|
645
|
+
}
|
|
925
646
|
};
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const r = await renderResultSets(ctx, partial, out);
|
|
980
|
-
stats.rowsAffected = r.rowsAffected;
|
|
981
|
-
stats.rowsPrinted = r.rowsPrinted;
|
|
982
|
-
}
|
|
983
|
-
catch {
|
|
984
|
-
// Surface the original error; don't shadow it with a render failure.
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
const message = recordError(ctx, err, sql);
|
|
988
|
-
writeQueryError(ctx, message);
|
|
989
|
-
stats.hadError = true;
|
|
990
|
-
}
|
|
991
|
-
finally {
|
|
992
|
-
if (ctx.settings.timing) {
|
|
993
|
-
stats.durationMs = performance.now() - started;
|
|
994
|
-
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
// Mirror upstream's `SetResultVariables` / `SetErrorVariables` call at the
|
|
998
|
-
// tail of `SendQuery`: refresh the per-statement diagnostic psql vars so
|
|
999
|
-
// `\echo :SQLSTATE` and friends see the most recent outcome. ROW_COUNT
|
|
1000
|
-
// tracks libpq's `PQcmdTuples` on the LAST result of a `\;` batch.
|
|
1001
|
-
refreshErrorVars(ctx.settings, stats.hadError
|
|
1002
|
-
? { kind: "error" }
|
|
1003
|
-
: { kind: "success", rowCount: lastRowCount });
|
|
1004
|
-
return stats;
|
|
647
|
+
const executeAndPrint = async (ctx, sqlRaw, opts = {}) => {
|
|
648
|
+
const sql = stripLeadingCommentsAndWS(sqlRaw);
|
|
649
|
+
const started = ctx.settings.timing ? performance.now() : 0;
|
|
650
|
+
const stats = {
|
|
651
|
+
rowsAffected: 0,
|
|
652
|
+
rowsPrinted: 0,
|
|
653
|
+
fetched: false,
|
|
654
|
+
hadError: false,
|
|
655
|
+
durationMs: 0
|
|
656
|
+
};
|
|
657
|
+
if (!ctx.settings.db) {
|
|
658
|
+
writeError(ctx, "no connection to the server");
|
|
659
|
+
stats.hadError = true;
|
|
660
|
+
return stats;
|
|
661
|
+
}
|
|
662
|
+
const out = pickOut(ctx, opts.oneShotOut);
|
|
663
|
+
const fetchCount = readFetchCount(ctx.settings);
|
|
664
|
+
let lastRowCount = null;
|
|
665
|
+
try {
|
|
666
|
+
if (fetchCount > 0 && isSelectCommand(sql)) {
|
|
667
|
+
const r = await runCursorLoop(ctx, sql, fetchCount, out);
|
|
668
|
+
stats.rowsAffected = r.rowsAffected;
|
|
669
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
670
|
+
stats.fetched = true;
|
|
671
|
+
lastRowCount = r.lastRowCount;
|
|
672
|
+
} else {
|
|
673
|
+
const results = await ctx.settings.db.execSimple(sql);
|
|
674
|
+
const r = await renderResultSets(ctx, results, out);
|
|
675
|
+
stats.rowsAffected = r.rowsAffected;
|
|
676
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
677
|
+
lastRowCount = r.lastRowCount;
|
|
678
|
+
}
|
|
679
|
+
} catch (err) {
|
|
680
|
+
const partial = err.partialResults;
|
|
681
|
+
if (partial && partial.length > 0) try {
|
|
682
|
+
const r = await renderResultSets(ctx, partial, out);
|
|
683
|
+
stats.rowsAffected = r.rowsAffected;
|
|
684
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
685
|
+
} catch {}
|
|
686
|
+
const message = recordError(ctx, err, sql);
|
|
687
|
+
writeQueryError(ctx, message);
|
|
688
|
+
stats.hadError = true;
|
|
689
|
+
} finally {
|
|
690
|
+
if (ctx.settings.timing) {
|
|
691
|
+
stats.durationMs = performance.now() - started;
|
|
692
|
+
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
refreshErrorVars(ctx.settings, stats.hadError ? { kind: "error" } : {
|
|
696
|
+
kind: "success",
|
|
697
|
+
rowCount: lastRowCount
|
|
698
|
+
});
|
|
699
|
+
return stats;
|
|
1005
700
|
};
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
catch (err) {
|
|
1122
|
-
// `\;`-chained batches surface every result the server produced before
|
|
1123
|
-
// the ErrorResponse on the thrown Error's `partialResults` field (set
|
|
1124
|
-
// by the wire layer's ReadyForQuery handler). Render them in order
|
|
1125
|
-
// before printing the error itself so the user sees the same shape
|
|
1126
|
-
// upstream `PQgetResult` walks produce.
|
|
1127
|
-
const partial = err
|
|
1128
|
-
.partialResults;
|
|
1129
|
-
if (partial && partial.length > 0) {
|
|
1130
|
-
try {
|
|
1131
|
-
const r = await renderResultSets(ctx, partial, out);
|
|
1132
|
-
stats.rowsAffected = r.rowsAffected;
|
|
1133
|
-
stats.rowsPrinted = r.rowsPrinted;
|
|
1134
|
-
}
|
|
1135
|
-
catch {
|
|
1136
|
-
// Surface the original error; don't shadow it with a render failure.
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
const message = recordError(ctx, err, sql);
|
|
1140
|
-
writeQueryError(ctx, message);
|
|
1141
|
-
stats.hadError = true;
|
|
1142
|
-
}
|
|
1143
|
-
// ----- ON_ERROR_ROLLBACK: resolve the savepoint --------------------------
|
|
1144
|
-
if (savepointIssued) {
|
|
1145
|
-
try {
|
|
1146
|
-
if (stats.hadError) {
|
|
1147
|
-
await db.execSimple(`ROLLBACK TO SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
1148
|
-
// Release the now-empty savepoint too, matching upstream.
|
|
1149
|
-
await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
1150
|
-
}
|
|
1151
|
-
else if (!destroysSavepoint(sql) && readTxStatus(db) === "T") {
|
|
1152
|
-
await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
catch (err) {
|
|
1156
|
-
// Don't shadow the original error; just record this one if we don't
|
|
1157
|
-
// already have one to report.
|
|
1158
|
-
if (!stats.hadError) {
|
|
1159
|
-
const message = recordError(ctx, err);
|
|
1160
|
-
writeQueryError(ctx, message);
|
|
1161
|
-
stats.hadError = true;
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
// If we issued an implicit BEGIN for AUTOCOMMIT=off and the statement
|
|
1166
|
-
// itself failed in such a way that we ended up idle again, there is
|
|
1167
|
-
// nothing to clean up — the server has already rolled back. We
|
|
1168
|
-
// intentionally do not COMMIT here: that's the user's responsibility under
|
|
1169
|
-
// AUTOCOMMIT=off.
|
|
1170
|
-
void implicitBeginIssued;
|
|
1171
|
-
// Mirror upstream `SendQuery` tail (common.c lines 1217-1218):
|
|
1172
|
-
//
|
|
1173
|
-
// if (!OK && pset.echo == PSQL_ECHO_ERRORS)
|
|
1174
|
-
// pg_log_info("STATEMENT: %s", query);
|
|
1175
|
-
//
|
|
1176
|
-
// When ECHO=errors and the dispatch failed, emit a `STATEMENT: <sql>`
|
|
1177
|
-
// line so the user can correlate the error with the input statement.
|
|
1178
|
-
// `pg_log_info` writes to stderr in upstream and strips one trailing
|
|
1179
|
-
// newline before tacking its own `\n` on the message — we mirror by
|
|
1180
|
-
// going through ctx.stderr and the explicit trim.
|
|
1181
|
-
if (stats.hadError && ctx.settings.echo === "errors") {
|
|
1182
|
-
// Strip leading whitespace + `--`-style comments from queryBuf so the
|
|
1183
|
-
// STATEMENT echo matches upstream's shape. Upstream `psqlscan.l`'s
|
|
1184
|
-
// `{whitespace}` rule (which includes line comments) SUPPRESSES
|
|
1185
|
-
// queryBuf appends until non-whitespace content has been collected;
|
|
1186
|
-
// our scanner accumulates verbatim. The server still ignores the
|
|
1187
|
-
// leading noise for `LINE N:` counting, but the STATEMENT echo
|
|
1188
|
-
// re-prints the buffer as we hold it. Bring them in line by
|
|
1189
|
-
// stripping here. Also strip one trailing `\n` to match
|
|
1190
|
-
// `pg_log_info("STATEMENT: %s", query)` (one-newline-strip +
|
|
1191
|
-
// explicit `\n` append).
|
|
1192
|
-
let stmt = sql;
|
|
1193
|
-
while (true) {
|
|
1194
|
-
const before = stmt.length;
|
|
1195
|
-
// Leading whitespace including form-feed (matches psqlscan's
|
|
1196
|
-
// {space} = [ \t\n\r\f]).
|
|
1197
|
-
stmt = stmt.replace(/^[ \t\n\r\f]+/, "");
|
|
1198
|
-
// Leading `--`-style line comment, up to (but not including) the
|
|
1199
|
-
// next newline. The trailing newline is then eaten by the next
|
|
1200
|
-
// whitespace pass.
|
|
1201
|
-
stmt = stmt.replace(/^--[^\n\r]*/, "");
|
|
1202
|
-
if (stmt.length === before)
|
|
1203
|
-
break;
|
|
1204
|
-
}
|
|
1205
|
-
if (stmt.endsWith("\n"))
|
|
1206
|
-
stmt = stmt.slice(0, -1);
|
|
1207
|
-
ctx.stderr.write(`STATEMENT: ${stmt}\n`);
|
|
1208
|
-
}
|
|
1209
|
-
// Mirror upstream's `SetResultVariables` / `SetErrorVariables` call at the
|
|
1210
|
-
// tail of `SendQuery`. ROW_COUNT mirrors libpq's `PQcmdTuples` on the LAST
|
|
1211
|
-
// result of a `\;` batch; SQLSTATE / ERROR reset every statement; the
|
|
1212
|
-
// LAST_ERROR_* pair only changes on failure (sticky on success).
|
|
1213
|
-
refreshErrorVars(ctx.settings, stats.hadError
|
|
1214
|
-
? { kind: "error" }
|
|
1215
|
-
: { kind: "success", rowCount: lastRowCount });
|
|
1216
|
-
if (ctx.settings.timing) {
|
|
1217
|
-
stats.durationMs = performance.now() - started;
|
|
1218
|
-
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
1219
|
-
}
|
|
1220
|
-
return stats;
|
|
701
|
+
const sendQuery = async (ctx, sqlRaw, opts = {}) => {
|
|
702
|
+
const sql = stripLeadingCommentsAndWS(sqlRaw);
|
|
703
|
+
const stats = {
|
|
704
|
+
rowsAffected: 0,
|
|
705
|
+
rowsPrinted: 0,
|
|
706
|
+
fetched: false,
|
|
707
|
+
hadError: false,
|
|
708
|
+
durationMs: 0
|
|
709
|
+
};
|
|
710
|
+
ctx.settings.lastQuery = sql;
|
|
711
|
+
if (!ctx.settings.db) {
|
|
712
|
+
writeError(ctx, "no connection to the server");
|
|
713
|
+
stats.hadError = true;
|
|
714
|
+
return stats;
|
|
715
|
+
}
|
|
716
|
+
if (readSinglestep(ctx.settings)) {
|
|
717
|
+
if (!await confirmSinglestep(ctx, sql)) {
|
|
718
|
+
stats.hadError = true;
|
|
719
|
+
ctx.settings.lastErrorResult = { message: "command cancelled by user" };
|
|
720
|
+
return stats;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const db = ctx.settings.db;
|
|
724
|
+
const autocommit = readAutocommit(ctx.settings);
|
|
725
|
+
const onErrorRollback = readOnErrorRollback(ctx.settings);
|
|
726
|
+
const interactive = !ctx.settings.notty;
|
|
727
|
+
const started = ctx.settings.timing ? performance.now() : 0;
|
|
728
|
+
if (!autocommit && readTxStatus(db) === "I" && !commandNoBegin(sql)) try {
|
|
729
|
+
await db.execSimple("BEGIN");
|
|
730
|
+
} catch (err) {
|
|
731
|
+
const message = recordError(ctx, err);
|
|
732
|
+
writeQueryError(ctx, message);
|
|
733
|
+
stats.hadError = true;
|
|
734
|
+
if (ctx.settings.timing) {
|
|
735
|
+
stats.durationMs = performance.now() - started;
|
|
736
|
+
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
737
|
+
}
|
|
738
|
+
return stats;
|
|
739
|
+
}
|
|
740
|
+
const savepointActive = onErrorRollback !== "off" && (onErrorRollback === "on" || onErrorRollback === "interactive" && interactive) && readTxStatus(db) === "T";
|
|
741
|
+
let savepointIssued = false;
|
|
742
|
+
if (savepointActive) try {
|
|
743
|
+
await db.execSimple(`SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
744
|
+
savepointIssued = true;
|
|
745
|
+
} catch (err) {
|
|
746
|
+
const message = recordError(ctx, err);
|
|
747
|
+
writeQueryError(ctx, message);
|
|
748
|
+
stats.hadError = true;
|
|
749
|
+
if (ctx.settings.timing) {
|
|
750
|
+
stats.durationMs = performance.now() - started;
|
|
751
|
+
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
752
|
+
}
|
|
753
|
+
return stats;
|
|
754
|
+
}
|
|
755
|
+
const out = pickOut(ctx, opts.oneShotOut);
|
|
756
|
+
const fetchCount = readFetchCount(ctx.settings);
|
|
757
|
+
let lastRowCount = null;
|
|
758
|
+
try {
|
|
759
|
+
if (fetchCount > 0 && isSelectCommand(sql)) {
|
|
760
|
+
const r = await runCursorLoop(ctx, sql, fetchCount, out);
|
|
761
|
+
stats.rowsAffected = r.rowsAffected;
|
|
762
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
763
|
+
stats.fetched = true;
|
|
764
|
+
lastRowCount = r.lastRowCount;
|
|
765
|
+
} else {
|
|
766
|
+
const results = await db.execSimple(sql);
|
|
767
|
+
const r = await renderResultSets(ctx, results, out);
|
|
768
|
+
stats.rowsAffected = r.rowsAffected;
|
|
769
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
770
|
+
lastRowCount = r.lastRowCount;
|
|
771
|
+
}
|
|
772
|
+
} catch (err) {
|
|
773
|
+
const partial = err.partialResults;
|
|
774
|
+
if (partial && partial.length > 0) try {
|
|
775
|
+
const r = await renderResultSets(ctx, partial, out);
|
|
776
|
+
stats.rowsAffected = r.rowsAffected;
|
|
777
|
+
stats.rowsPrinted = r.rowsPrinted;
|
|
778
|
+
} catch {}
|
|
779
|
+
const message = recordError(ctx, err, sql);
|
|
780
|
+
writeQueryError(ctx, message);
|
|
781
|
+
stats.hadError = true;
|
|
782
|
+
}
|
|
783
|
+
if (savepointIssued) try {
|
|
784
|
+
if (stats.hadError) {
|
|
785
|
+
await db.execSimple(`ROLLBACK TO SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
786
|
+
await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
787
|
+
} else if (!destroysSavepoint(sql) && readTxStatus(db) === "T") await db.execSimple(`RELEASE SAVEPOINT ${SAVEPOINT_NAME}`);
|
|
788
|
+
} catch (err) {
|
|
789
|
+
if (!stats.hadError) {
|
|
790
|
+
const message = recordError(ctx, err);
|
|
791
|
+
writeQueryError(ctx, message);
|
|
792
|
+
stats.hadError = true;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (stats.hadError && ctx.settings.echo === "errors") {
|
|
796
|
+
let stmt = sql;
|
|
797
|
+
while (true) {
|
|
798
|
+
const before = stmt.length;
|
|
799
|
+
stmt = stmt.replace(/^[ \t\n\r\f]+/, "");
|
|
800
|
+
stmt = stmt.replace(/^--[^\n\r]*/, "");
|
|
801
|
+
if (stmt.length === before) break;
|
|
802
|
+
}
|
|
803
|
+
if (stmt.endsWith("\n")) stmt = stmt.slice(0, -1);
|
|
804
|
+
ctx.stderr.write(`STATEMENT: ${stmt}\n`);
|
|
805
|
+
}
|
|
806
|
+
refreshErrorVars(ctx.settings, stats.hadError ? { kind: "error" } : {
|
|
807
|
+
kind: "success",
|
|
808
|
+
rowCount: lastRowCount
|
|
809
|
+
});
|
|
810
|
+
if (ctx.settings.timing) {
|
|
811
|
+
stats.durationMs = performance.now() - started;
|
|
812
|
+
ctx.stdout.write("\n" + formatDurationMs(stats.durationMs) + "\n");
|
|
813
|
+
}
|
|
814
|
+
return stats;
|
|
1221
815
|
};
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
try {
|
|
1232
|
-
const sets = await conn.execSimple(sql);
|
|
1233
|
-
if (sets.length === 0)
|
|
1234
|
-
return null;
|
|
1235
|
-
return sets[sets.length - 1];
|
|
1236
|
-
}
|
|
1237
|
-
catch (err) {
|
|
1238
|
-
if (ignoreError)
|
|
1239
|
-
return null;
|
|
1240
|
-
throw err;
|
|
1241
|
-
}
|
|
816
|
+
const psqlExec = async (conn, sql, ignoreError = false) => {
|
|
817
|
+
try {
|
|
818
|
+
const sets = await conn.execSimple(sql);
|
|
819
|
+
if (sets.length === 0) return null;
|
|
820
|
+
return sets[sets.length - 1];
|
|
821
|
+
} catch (err) {
|
|
822
|
+
if (ignoreError) return null;
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
1242
825
|
};
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
peekKeywords,
|
|
1251
|
-
SAVEPOINT_NAME,
|
|
1252
|
-
CURSOR_NAME,
|
|
826
|
+
const __testing = {
|
|
827
|
+
commandNoBegin,
|
|
828
|
+
isSelectCommand,
|
|
829
|
+
destroysSavepoint,
|
|
830
|
+
peekKeywords,
|
|
831
|
+
SAVEPOINT_NAME,
|
|
832
|
+
CURSOR_NAME
|
|
1253
833
|
};
|
|
834
|
+
//#endregion
|
|
835
|
+
export { __testing, captureLastError, executeAndPrint, pickOut, psqlExec, refreshErrorVars, renderResultSet, sendQuery, stripLeadingCommentsAndWS, writeQueryError };
|