neonctl 2.22.0 → 2.23.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 +242 -16
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/checkout.js +249 -0
- package/commands/connection_string.js +15 -2
- package/commands/data_api.js +286 -0
- package/commands/functions.js +277 -0
- package/commands/index.js +12 -0
- package/commands/link.js +667 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +62 -0
- package/commands/set_context.js +7 -2
- package/context.js +86 -14
- package/functions_api.js +44 -0
- package/index.js +3 -0
- package/package.json +60 -51
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/enrichers.js +18 -1
- package/utils/esbuild.js +147 -0
- package/utils/middlewares.js +1 -1
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/connection_string.test.js +0 -196
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pg_service.conf` (connection service file) support.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of libpq's `parseServiceInfo` in `src/interfaces/libpq/
|
|
5
|
+
* fe-connect.c`. A service file is an INI-style document with one section
|
|
6
|
+
* per service name; each section's `key=value` pairs map onto libpq
|
|
7
|
+
* connection parameters.
|
|
8
|
+
*
|
|
9
|
+
* Format:
|
|
10
|
+
*
|
|
11
|
+
* # this is a comment
|
|
12
|
+
* [servicename]
|
|
13
|
+
* host=foo.example.com
|
|
14
|
+
* port=5432
|
|
15
|
+
* dbname=mydb
|
|
16
|
+
* user=myuser
|
|
17
|
+
*
|
|
18
|
+
* [other]
|
|
19
|
+
* host=bar
|
|
20
|
+
*
|
|
21
|
+
* Resolution order (mirrors libpq):
|
|
22
|
+
*
|
|
23
|
+
* 1. `$PGSERVICEFILE` if set and non-empty
|
|
24
|
+
* 2. `~/.pg_service.conf` (POSIX) / `%APPDATA%\postgresql\.pg_service.conf`
|
|
25
|
+
* (Windows)
|
|
26
|
+
* 3. `$PGSYSCONFDIR/pg_service.conf`
|
|
27
|
+
* 4. `/etc/pg_service.conf` (POSIX platform-default fallback)
|
|
28
|
+
*
|
|
29
|
+
* Files are stat'd in order; the first existing file wins. (libpq merges
|
|
30
|
+
* service entries across files in load order; we read only the first
|
|
31
|
+
* existing one to match what neonctl users actually rely on. This matches
|
|
32
|
+
* the documented "search path" behaviour.)
|
|
33
|
+
*
|
|
34
|
+
* Keys in a service section are case-sensitive; the section header is the
|
|
35
|
+
* service name (libpq normalises neither). Unknown keys are accepted
|
|
36
|
+
* silently — libpq would also accept them and feed them to `PQconninfoParse`,
|
|
37
|
+
* which is responsible for the recognised-key gate.
|
|
38
|
+
*/
|
|
39
|
+
import { promises as fs } from 'node:fs';
|
|
40
|
+
import * as os from 'node:os';
|
|
41
|
+
import * as path from 'node:path';
|
|
42
|
+
const isWindows = process.platform === 'win32';
|
|
43
|
+
/**
|
|
44
|
+
* Return the ordered list of candidate `pg_service.conf` paths to try. The
|
|
45
|
+
* caller stops at the first one that exists. Pure function — `env` defaults
|
|
46
|
+
* to `process.env` for ergonomics but can be injected for tests.
|
|
47
|
+
*/
|
|
48
|
+
export const defaultPgServiceFilePath = (env = process.env) => {
|
|
49
|
+
const out = [];
|
|
50
|
+
const explicit = env.PGSERVICEFILE;
|
|
51
|
+
if (explicit !== undefined && explicit.length > 0) {
|
|
52
|
+
out.push(explicit);
|
|
53
|
+
}
|
|
54
|
+
// User-level: `~/.pg_service.conf` (POSIX) or
|
|
55
|
+
// `%APPDATA%/postgresql/.pg_service.conf` (Windows). Matches libpq.
|
|
56
|
+
if (isWindows) {
|
|
57
|
+
const appdata = env.APPDATA;
|
|
58
|
+
if (appdata !== undefined && appdata.length > 0) {
|
|
59
|
+
out.push(path.join(appdata, 'postgresql', '.pg_service.conf'));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const home = env.HOME ?? os.homedir();
|
|
64
|
+
if (home.length > 0) {
|
|
65
|
+
out.push(path.join(home, '.pg_service.conf'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// System-level: `$PGSYSCONFDIR/pg_service.conf`.
|
|
69
|
+
const sysDir = env.PGSYSCONFDIR;
|
|
70
|
+
if (sysDir !== undefined && sysDir.length > 0) {
|
|
71
|
+
out.push(path.join(sysDir, 'pg_service.conf'));
|
|
72
|
+
}
|
|
73
|
+
// Platform-default system path. libpq's autoconf picks SYSCONFDIR at build
|
|
74
|
+
// time; for a portable TS implementation we use `/etc/pg_service.conf` on
|
|
75
|
+
// POSIX. Windows has no canonical equivalent.
|
|
76
|
+
if (!isWindows) {
|
|
77
|
+
out.push('/etc/pg_service.conf');
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Parse the contents of a `pg_service.conf` string into a Map keyed by
|
|
83
|
+
* service name. Exported for tests; production callers should use
|
|
84
|
+
* `loadPgServices` which walks the discovery list.
|
|
85
|
+
*
|
|
86
|
+
* Parser behaviour:
|
|
87
|
+
* - Lines starting with `#` (after leading whitespace) are comments.
|
|
88
|
+
* - Blank lines are skipped.
|
|
89
|
+
* - Section header `[name]` opens a new service; later sections with the
|
|
90
|
+
* same name OVERRIDE earlier ones within the same file (libpq emits a
|
|
91
|
+
* warning; we silently override).
|
|
92
|
+
* - `key=value` lines populate the current section. Leading/trailing
|
|
93
|
+
* whitespace around `key` and `value` is trimmed; values are NOT quoted
|
|
94
|
+
* (libpq's parser is line-oriented).
|
|
95
|
+
* - Lines outside any section header are silently ignored.
|
|
96
|
+
*/
|
|
97
|
+
export const parsePgServiceContent = (content) => {
|
|
98
|
+
const services = new Map();
|
|
99
|
+
let current;
|
|
100
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
101
|
+
const line = rawLine.trim();
|
|
102
|
+
if (line.length === 0)
|
|
103
|
+
continue;
|
|
104
|
+
if (line.startsWith('#'))
|
|
105
|
+
continue;
|
|
106
|
+
if (line.startsWith('[')) {
|
|
107
|
+
const close = line.indexOf(']');
|
|
108
|
+
if (close < 0) {
|
|
109
|
+
// Malformed header — libpq aborts the file; we mirror by ending the
|
|
110
|
+
// current section so the bogus line doesn't bleed into it.
|
|
111
|
+
current = undefined;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const name = line.slice(1, close).trim();
|
|
115
|
+
if (name.length === 0) {
|
|
116
|
+
current = undefined;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
current = {};
|
|
120
|
+
services.set(name, current);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (current === undefined) {
|
|
124
|
+
// Stray key=value before any [section] header — libpq treats this as
|
|
125
|
+
// an error; we silently skip so a misformatted file doesn't refuse to
|
|
126
|
+
// resolve services that appear after the noise.
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const eq = line.indexOf('=');
|
|
130
|
+
if (eq < 0)
|
|
131
|
+
continue;
|
|
132
|
+
const key = line.slice(0, eq).trim();
|
|
133
|
+
const value = line.slice(eq + 1).trim();
|
|
134
|
+
if (key.length === 0)
|
|
135
|
+
continue;
|
|
136
|
+
current[key] = value;
|
|
137
|
+
}
|
|
138
|
+
return services;
|
|
139
|
+
};
|
|
140
|
+
const readIfExists = async (filePath) => {
|
|
141
|
+
try {
|
|
142
|
+
return await fs.readFile(filePath, 'utf8');
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const code = err.code;
|
|
146
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
147
|
+
return null;
|
|
148
|
+
// Permission / I/O errors silently degrade — libpq treats an unreadable
|
|
149
|
+
// service file the same as a missing one (the connection falls back to
|
|
150
|
+
// other parameter sources).
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Discover and parse `pg_service.conf`. Walks the candidate list returned by
|
|
156
|
+
* `defaultPgServiceFilePath`, reading the first existing file.
|
|
157
|
+
*
|
|
158
|
+
* Resolves to an empty Map when no file is found. When the user has
|
|
159
|
+
* explicitly named a file via `$PGSERVICEFILE` and that file is missing,
|
|
160
|
+
* throws an error with the upstream wording — mirrors libpq's
|
|
161
|
+
* "service file %s not found" diagnostic (different from a silent
|
|
162
|
+
* discovery-chain miss, where each candidate may legitimately be absent).
|
|
163
|
+
*
|
|
164
|
+
* The `env` argument is exposed so tests can drive the user-specified
|
|
165
|
+
* branch without touching `process.env`. Production callers leave it
|
|
166
|
+
* defaulting to `process.env`.
|
|
167
|
+
*/
|
|
168
|
+
export const loadPgServices = async (paths, env = process.env) => {
|
|
169
|
+
const candidates = paths ?? defaultPgServiceFilePath(env);
|
|
170
|
+
const userSpecified = env.PGSERVICEFILE;
|
|
171
|
+
const userSpecifiedAbs = userSpecified !== undefined && userSpecified.length > 0
|
|
172
|
+
? userSpecified
|
|
173
|
+
: null;
|
|
174
|
+
for (const p of candidates) {
|
|
175
|
+
const content = await readIfExists(p);
|
|
176
|
+
if (content === null) {
|
|
177
|
+
// Hard-fail when the user explicitly named this file (via
|
|
178
|
+
// PGSERVICEFILE) and it's missing. Silent ENOENT is fine for the
|
|
179
|
+
// discovery-chain candidates the user didn't ask for.
|
|
180
|
+
if (userSpecifiedAbs !== null && p === userSpecifiedAbs) {
|
|
181
|
+
throw new Error(`service file "${p}" not found`);
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
return parsePgServiceContent(content);
|
|
186
|
+
}
|
|
187
|
+
return new Map();
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Look up a service section by name. Returns `undefined` if not present.
|
|
191
|
+
*
|
|
192
|
+
* Service names are case-sensitive (libpq does not normalise).
|
|
193
|
+
*/
|
|
194
|
+
export const lookupService = (services, name) => services.get(name);
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.psqlrc` autoload.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of `process_psqlrc()` / `process_psqlrc_file()` in
|
|
5
|
+
* `src/bin/psql/startup.c`. Two responsibilities:
|
|
6
|
+
*
|
|
7
|
+
* 1. `defaultPsqlrcPath(env?)` — pure function returning the path psql
|
|
8
|
+
* *would* read in the absence of a `$PSQLRC` override. Resolves to
|
|
9
|
+
* `$HOME/.psqlrc` on POSIX and `%APPDATA%/postgresql/psqlrc.conf` on
|
|
10
|
+
* Windows (the upstream layout).
|
|
11
|
+
*
|
|
12
|
+
* 2. `loadPsqlrc(ctx)` — discover and execute the rc files in upstream
|
|
13
|
+
* order:
|
|
14
|
+
* a. `$PGSYSCONFDIR/psqlrc-VERSION` and `$PGSYSCONFDIR/psqlrc`
|
|
15
|
+
* b. `$PSQLRC` if non-empty (overrides $HOME path)
|
|
16
|
+
* c. else `$HOME/.psqlrc-VERSION` then `$HOME/.psqlrc`
|
|
17
|
+
* Each existing file's contents are split into statements via the same
|
|
18
|
+
* `scanSql` boundary detector the mainloop uses, then dispatched
|
|
19
|
+
* through `connection.execSimple` (for SQL) or the backslash registry
|
|
20
|
+
* (for `\…` commands).
|
|
21
|
+
*
|
|
22
|
+
* Duplication note: we deliberately re-implement a minimal scan-and-dispatch
|
|
23
|
+
* loop here instead of routing through `core/mainloop.ts`. The mainloop is
|
|
24
|
+
* locked-down in WP-12 and adding an `executeInputString` helper to it would
|
|
25
|
+
* touch a file we can't modify in this WP. The two loops share `scanSql` and
|
|
26
|
+
* `dispatchBackslash` so the divergence is structural, not behavioural.
|
|
27
|
+
*
|
|
28
|
+
* Follow-up issue: refactor `mainloop.ts` to expose an `executeInputString`
|
|
29
|
+
* primitive and delete this duplication. Tracked TODO at the bottom of this
|
|
30
|
+
* file.
|
|
31
|
+
*/
|
|
32
|
+
import { promises as fs } from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { initialScanState } from '../types/scanner.js';
|
|
35
|
+
import { scanSql } from '../scanner/sql.js';
|
|
36
|
+
import { scanSlashArgs } from '../scanner/slash.js';
|
|
37
|
+
import { dispatchBackslash } from '../command/dispatch.js';
|
|
38
|
+
import { attachCondStack, COND_COMMAND_NAMES } from '../command/cmd_cond.js';
|
|
39
|
+
import { sendQuery } from '../core/common.js';
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Path discovery.
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
const isWindows = process.platform === 'win32';
|
|
44
|
+
const baseRcName = isWindows ? 'psqlrc.conf' : '.psqlrc';
|
|
45
|
+
/**
|
|
46
|
+
* Return the default user-level psqlrc path. The path is *not* verified to
|
|
47
|
+
* exist — callers stat it lazily. The Windows layout follows upstream:
|
|
48
|
+
* `%APPDATA%/postgresql/psqlrc.conf`. On POSIX we use `$HOME/.psqlrc`.
|
|
49
|
+
*/
|
|
50
|
+
export const defaultPsqlrcPath = (env = process.env) => {
|
|
51
|
+
if (isWindows) {
|
|
52
|
+
const appData = env.APPDATA;
|
|
53
|
+
if (appData && appData.length > 0) {
|
|
54
|
+
return path.join(appData, 'postgresql', baseRcName);
|
|
55
|
+
}
|
|
56
|
+
return path.join(env.USERPROFILE ?? '', 'postgresql', baseRcName);
|
|
57
|
+
}
|
|
58
|
+
const home = env.HOME ?? '';
|
|
59
|
+
return path.join(home, baseRcName);
|
|
60
|
+
};
|
|
61
|
+
const versionSuffix = (serverVersion) => {
|
|
62
|
+
if (!serverVersion || serverVersion <= 0)
|
|
63
|
+
return null;
|
|
64
|
+
// PG numeric version is e.g. 170002 (17.2). Upstream uses both
|
|
65
|
+
// `psqlrc-PG_VERSION` (major.minor) and `psqlrc-PG_MAJORVERSION`. We use
|
|
66
|
+
// the major (e.g. 17) — the bigger of the two backwards-compat targets.
|
|
67
|
+
const major = Math.floor(serverVersion / 10000);
|
|
68
|
+
return String(major);
|
|
69
|
+
};
|
|
70
|
+
export const psqlrcCandidates = (env, serverVersion) => {
|
|
71
|
+
const out = [];
|
|
72
|
+
const suffix = versionSuffix(serverVersion);
|
|
73
|
+
// System-wide.
|
|
74
|
+
const sysDir = env.PGSYSCONFDIR;
|
|
75
|
+
if (sysDir && sysDir.length > 0) {
|
|
76
|
+
const sysBase = isWindows ? 'psqlrc.conf' : 'psqlrc';
|
|
77
|
+
if (suffix) {
|
|
78
|
+
out.push({
|
|
79
|
+
path: path.join(sysDir, `${sysBase}-${suffix}`),
|
|
80
|
+
description: 'system psqlrc (versioned)',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
out.push({
|
|
84
|
+
path: path.join(sysDir, sysBase),
|
|
85
|
+
description: 'system psqlrc',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// PSQLRC override suppresses HOME-based discovery (matches upstream).
|
|
89
|
+
const envRc = env.PSQLRC;
|
|
90
|
+
if (envRc !== undefined && envRc !== '') {
|
|
91
|
+
out.push({ path: expandTilde(envRc, env), description: '$PSQLRC' });
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
// HOME-based.
|
|
95
|
+
const home = env.HOME ?? '';
|
|
96
|
+
if (isWindows) {
|
|
97
|
+
const appData = env.APPDATA;
|
|
98
|
+
const dir = appData && appData.length > 0
|
|
99
|
+
? path.join(appData, 'postgresql')
|
|
100
|
+
: path.join(env.USERPROFILE ?? '', 'postgresql');
|
|
101
|
+
if (suffix) {
|
|
102
|
+
out.push({
|
|
103
|
+
path: path.join(dir, `${baseRcName}-${suffix}`),
|
|
104
|
+
description: 'user psqlrc (versioned)',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
out.push({
|
|
108
|
+
path: path.join(dir, baseRcName),
|
|
109
|
+
description: 'user psqlrc',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (home.length > 0) {
|
|
113
|
+
if (suffix) {
|
|
114
|
+
out.push({
|
|
115
|
+
path: path.join(home, `${baseRcName}-${suffix}`),
|
|
116
|
+
description: 'user psqlrc (versioned)',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
out.push({
|
|
120
|
+
path: path.join(home, baseRcName),
|
|
121
|
+
description: 'user psqlrc',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
const expandTilde = (p, env) => {
|
|
127
|
+
if (p.startsWith('~/') && env.HOME) {
|
|
128
|
+
return path.join(env.HOME, p.slice(2));
|
|
129
|
+
}
|
|
130
|
+
if (p === '~' && env.HOME) {
|
|
131
|
+
return env.HOME;
|
|
132
|
+
}
|
|
133
|
+
return p;
|
|
134
|
+
};
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// File read + dispatch.
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
const readIfExists = async (file) => {
|
|
139
|
+
try {
|
|
140
|
+
return await fs.readFile(file, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const code = err.code;
|
|
144
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
145
|
+
return null;
|
|
146
|
+
// Permission / I/O errors propagate — they're worth surfacing because
|
|
147
|
+
// they signal a likely misconfiguration. Mirror upstream which prints
|
|
148
|
+
// a warning but doesn't abort the session; we use a noop for now.
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const makeBackslashContext = (ctx, cmdName, rawArgs, queryBuf) => {
|
|
153
|
+
const varLookup = (name) => ctx.settings.vars.get(name);
|
|
154
|
+
const buffered = new Map();
|
|
155
|
+
const cursors = new Map();
|
|
156
|
+
const argsFor = (mode) => {
|
|
157
|
+
const cached = buffered.get(mode);
|
|
158
|
+
if (cached)
|
|
159
|
+
return cached;
|
|
160
|
+
const parsed = scanSlashArgs(rawArgs, mode, varLookup);
|
|
161
|
+
buffered.set(mode, parsed);
|
|
162
|
+
return parsed;
|
|
163
|
+
};
|
|
164
|
+
const bctx = {
|
|
165
|
+
settings: ctx.settings,
|
|
166
|
+
cmdName,
|
|
167
|
+
queryBuf,
|
|
168
|
+
rawArgs,
|
|
169
|
+
nextArg(mode = 'normal') {
|
|
170
|
+
const av = argsFor(mode);
|
|
171
|
+
const idx = cursors.get(mode) ?? 0;
|
|
172
|
+
if (idx >= av.length)
|
|
173
|
+
return null;
|
|
174
|
+
cursors.set(mode, idx + 1);
|
|
175
|
+
return av[idx];
|
|
176
|
+
},
|
|
177
|
+
restOfLine() {
|
|
178
|
+
return rawArgs;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
attachCondStack(bctx, ctx.cond);
|
|
182
|
+
return bctx;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Execute the supplied input string against the running REPL context. Used
|
|
186
|
+
* by `loadPsqlrc` (and exported for tests). This is a minimal cousin of
|
|
187
|
+
* `runMainLoop`'s processChunk — see the module header for the duplication
|
|
188
|
+
* rationale.
|
|
189
|
+
*
|
|
190
|
+
* Returns when EOF is reached; `\q` and other exit commands inside an rc
|
|
191
|
+
* file are treated as "stop reading this file" (the REPL itself continues).
|
|
192
|
+
*
|
|
193
|
+
* The returned outcome lets `runPsql`'s `-c`/`-f` loop apply upstream's
|
|
194
|
+
* per-switch exit-code semantics (see comment above).
|
|
195
|
+
*/
|
|
196
|
+
export const executeInputString = async (input, ctx, opts = {}) => {
|
|
197
|
+
let working = input;
|
|
198
|
+
let queryBuf = '';
|
|
199
|
+
let scanState = initialScanState();
|
|
200
|
+
let hadError = false;
|
|
201
|
+
let stoppedOnError = false;
|
|
202
|
+
let connectionLost = false;
|
|
203
|
+
const print = opts.print ?? false;
|
|
204
|
+
const noteConnectionLost = () => {
|
|
205
|
+
if (ctx.settings.db?.isClosed()) {
|
|
206
|
+
ctx.stderr.write('psql: error: connection to server was lost\n');
|
|
207
|
+
connectionLost = true;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
};
|
|
212
|
+
// Inline `:NAME` substitution wired through SQL bodies (handled by
|
|
213
|
+
// scanSql). Slash-arg bodies are already substituted via
|
|
214
|
+
// `makeBackslashContext` above. Mirror the mainloop's wiring so `-c`/`-f`
|
|
215
|
+
// / .psqlrc inputs expand the same way as interactive input.
|
|
216
|
+
const sqlVarLookup = (name) => ctx.settings.vars.get(name);
|
|
217
|
+
while (working.length > 0) {
|
|
218
|
+
const r = scanSql(working, scanState, sqlVarLookup);
|
|
219
|
+
scanState = r.nextState;
|
|
220
|
+
if (r.kind === 'semicolon') {
|
|
221
|
+
// Use the substituted text rather than the raw slice so `:NAME`
|
|
222
|
+
// expansions land in the executed SQL.
|
|
223
|
+
const sqlText = queryBuf + r.sql;
|
|
224
|
+
queryBuf = '';
|
|
225
|
+
working = working.slice(r.consumed);
|
|
226
|
+
scanState = initialScanState();
|
|
227
|
+
if (!ctx.cond.isActive())
|
|
228
|
+
continue;
|
|
229
|
+
const trimmed = sqlText.trim();
|
|
230
|
+
if (trimmed.length === 0)
|
|
231
|
+
continue;
|
|
232
|
+
if (!ctx.settings.db)
|
|
233
|
+
continue;
|
|
234
|
+
if (print) {
|
|
235
|
+
// `-c` / `-f`: route through the full SendQuery pipeline so SELECT
|
|
236
|
+
// tuples, NOTICE messages and `\timing` land on the output streams.
|
|
237
|
+
const stats = await sendQuery(ctx, sqlText);
|
|
238
|
+
// LATCH: a multi-statement input must report an error if ANY statement
|
|
239
|
+
// failed, not just the last one — otherwise `-c "SELECT 1; SELECT 1/0;
|
|
240
|
+
// SELECT 2"` exits 0 and silently passes CI. The
|
|
241
|
+
// -f/psqlrc callers ignore this aggregate (they only act on
|
|
242
|
+
// stoppedOnError), so latching is safe across all callers.
|
|
243
|
+
if (stats.hadError)
|
|
244
|
+
hadError = true;
|
|
245
|
+
if (noteConnectionLost()) {
|
|
246
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
247
|
+
}
|
|
248
|
+
if (stats.hadError && ctx.settings.onErrorStop) {
|
|
249
|
+
stoppedOnError = true;
|
|
250
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
try {
|
|
255
|
+
await ctx.settings.db.execSimple(sqlText);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
259
|
+
ctx.stderr.write(`psql: ERROR: ${msg}\n`);
|
|
260
|
+
hadError = true;
|
|
261
|
+
if (noteConnectionLost()) {
|
|
262
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
263
|
+
}
|
|
264
|
+
if (ctx.settings.onErrorStop) {
|
|
265
|
+
stoppedOnError = true;
|
|
266
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (r.kind === 'backslash') {
|
|
273
|
+
// Use `r.sql` — the scanner's substituted/processed text that
|
|
274
|
+
// *preceded* the backslash boundary. The raw consumed slice would
|
|
275
|
+
// also include the backslash command itself and any separators
|
|
276
|
+
// (like the `\\` end-of-args marker), which we don't want in the
|
|
277
|
+
// query buffer. `r.sql` excludes both. Mirrors the mainloop's
|
|
278
|
+
// `queryBuf += result.sql` path.
|
|
279
|
+
queryBuf += r.sql;
|
|
280
|
+
working = working.slice(r.consumed);
|
|
281
|
+
const cmdName = r.cmd;
|
|
282
|
+
// Skip cond-commands inside rc — they require the full mainloop state
|
|
283
|
+
// machine to be useful; we treat them as no-ops here. Other commands
|
|
284
|
+
// run through the standard registry.
|
|
285
|
+
if (COND_COMMAND_NAMES.has(cmdName))
|
|
286
|
+
continue;
|
|
287
|
+
if (!ctx.cond.isActive())
|
|
288
|
+
continue;
|
|
289
|
+
const bctx = makeBackslashContext(ctx, cmdName, r.rest, queryBuf);
|
|
290
|
+
let res;
|
|
291
|
+
try {
|
|
292
|
+
res = await dispatchBackslash(ctx.registry, cmdName, bctx);
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
296
|
+
ctx.stderr.write(`psql: ERROR: ${msg}\n`);
|
|
297
|
+
hadError = true;
|
|
298
|
+
if (ctx.settings.onErrorStop) {
|
|
299
|
+
stoppedOnError = true;
|
|
300
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (res.status === 'exit')
|
|
305
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
306
|
+
if (res.status === 'reset-buf') {
|
|
307
|
+
queryBuf = res.newBuf ?? '';
|
|
308
|
+
scanState = initialScanState();
|
|
309
|
+
}
|
|
310
|
+
// Mirror upstream `mainloop.c`: a backslash command that errored
|
|
311
|
+
// drops the query buffer and resets the scanner. Without this the
|
|
312
|
+
// residue would re-execute via the tail-dispatch at EOF (or get
|
|
313
|
+
// glued onto the next dispatched statement), masking the failure.
|
|
314
|
+
if (res.status === 'error') {
|
|
315
|
+
queryBuf = '';
|
|
316
|
+
scanState = initialScanState();
|
|
317
|
+
}
|
|
318
|
+
if (res.status === 'error')
|
|
319
|
+
hadError = true; // latch
|
|
320
|
+
if (noteConnectionLost()) {
|
|
321
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
322
|
+
}
|
|
323
|
+
if (res.status === 'error' && ctx.settings.onErrorStop) {
|
|
324
|
+
stoppedOnError = true;
|
|
325
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
// eof / incomplete: stash residue and stop. Use `r.sql` so any `:NAME`
|
|
330
|
+
// tokens fully consumed in this chunk make it into the buffer in
|
|
331
|
+
// substituted form. (Same caveat as the mainloop: cross-chunk colon
|
|
332
|
+
// references fall back to the literal.)
|
|
333
|
+
queryBuf += r.sql;
|
|
334
|
+
working = '';
|
|
335
|
+
}
|
|
336
|
+
// Tail dispatch: if the file ended mid-statement (no trailing `;`), run
|
|
337
|
+
// the residue. This matches `process_file` behaviour in upstream.
|
|
338
|
+
const tail = queryBuf.trim();
|
|
339
|
+
if (tail.length > 0 && ctx.settings.db && ctx.cond.isActive()) {
|
|
340
|
+
if (print) {
|
|
341
|
+
const stats = await sendQuery(ctx, queryBuf);
|
|
342
|
+
// LATCH, same as the semicolon branch — a trailing statement (no `;`)
|
|
343
|
+
// that succeeds must not clear an earlier error, or `-c "bad; ok"`
|
|
344
|
+
// wrongly exits 0. The function's latch invariant applies
|
|
345
|
+
// to the tail path too.
|
|
346
|
+
if (stats.hadError)
|
|
347
|
+
hadError = true;
|
|
348
|
+
noteConnectionLost();
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
try {
|
|
352
|
+
await ctx.settings.db.execSimple(queryBuf);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
356
|
+
ctx.stderr.write(`psql: ERROR: ${msg}\n`);
|
|
357
|
+
hadError = true;
|
|
358
|
+
noteConnectionLost();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return { hadError, stoppedOnError, connectionLost };
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Discover and execute `.psqlrc` files against `ctx`. Honors the `-X` flag
|
|
366
|
+
* via `opts.skip = true`. Safe to call before the connection is ready —
|
|
367
|
+
* commands that need `db` will simply no-op (see `executeInputString`).
|
|
368
|
+
*/
|
|
369
|
+
export const loadPsqlrc = async (ctx, opts = {}) => {
|
|
370
|
+
if (opts.skip)
|
|
371
|
+
return;
|
|
372
|
+
const env = opts.env ?? process.env;
|
|
373
|
+
// Single-path mode (test ergonomics / explicit override).
|
|
374
|
+
if (opts.path !== undefined) {
|
|
375
|
+
const content = await readIfExists(opts.path);
|
|
376
|
+
if (content === null)
|
|
377
|
+
return;
|
|
378
|
+
const prevSource = ctx.settings.curCmdSource;
|
|
379
|
+
ctx.settings.curCmdSource = 'rcfile';
|
|
380
|
+
try {
|
|
381
|
+
await executeInputString(content, ctx);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
ctx.settings.curCmdSource = prevSource;
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const serverVersion = ctx.settings.db?.serverVersion;
|
|
389
|
+
const candidates = psqlrcCandidates(env, serverVersion);
|
|
390
|
+
// Upstream `process_psqlrc()` is first-match-wins PER LOCATION: it reads at
|
|
391
|
+
// most ONE system file (versioned preferred over unversioned) and then at
|
|
392
|
+
// most ONE user file. Running both the versioned AND unversioned file in a
|
|
393
|
+
// location double-executes side effects and lets the unversioned file
|
|
394
|
+
// clobber the versioned one.
|
|
395
|
+
let systemDone = false;
|
|
396
|
+
for (const c of candidates) {
|
|
397
|
+
const isSystem = c.description.startsWith('system');
|
|
398
|
+
if (isSystem && systemDone)
|
|
399
|
+
continue; // first system file already ran
|
|
400
|
+
const content = await readIfExists(c.path);
|
|
401
|
+
if (content === null)
|
|
402
|
+
continue;
|
|
403
|
+
const prevSource = ctx.settings.curCmdSource;
|
|
404
|
+
ctx.settings.curCmdSource = 'rcfile';
|
|
405
|
+
try {
|
|
406
|
+
await executeInputString(content, ctx);
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
ctx.settings.curCmdSource = prevSource;
|
|
410
|
+
}
|
|
411
|
+
if (isSystem) {
|
|
412
|
+
systemDone = true; // consumed the system location; move to the user one
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
// First user / $PSQLRC match wins; nothing further to read.
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
// TODO(WP-26-followup): Refactor `core/mainloop.ts` to export an
|
|
420
|
+
// `executeInputString(input, ctx)` primitive, then have this module call it
|
|
421
|
+
// instead of duplicating the scan-and-dispatch loop. The duplication today is
|
|
422
|
+
// roughly 60 LOC; the refactor would shrink this file by half.
|