neonctl 2.22.2 → 2.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +84 -0
  2. package/analytics.js +5 -2
  3. package/commands/branches.js +9 -1
  4. package/commands/connection_string.js +9 -1
  5. package/commands/functions.js +268 -0
  6. package/commands/index.js +4 -0
  7. package/commands/neon_auth.js +1013 -0
  8. package/commands/projects.js +9 -1
  9. package/commands/psql.js +6 -1
  10. package/functions_api.js +43 -0
  11. package/package.json +15 -5
  12. package/psql/cli.js +51 -0
  13. package/psql/command/cmd_cond.js +437 -0
  14. package/psql/command/cmd_connect.js +815 -0
  15. package/psql/command/cmd_copy.js +1025 -0
  16. package/psql/command/cmd_describe.js +1810 -0
  17. package/psql/command/cmd_format.js +909 -0
  18. package/psql/command/cmd_io.js +2187 -0
  19. package/psql/command/cmd_lo.js +385 -0
  20. package/psql/command/cmd_meta.js +970 -0
  21. package/psql/command/cmd_misc.js +187 -0
  22. package/psql/command/cmd_pipeline.js +1141 -0
  23. package/psql/command/cmd_restrict.js +171 -0
  24. package/psql/command/cmd_show.js +751 -0
  25. package/psql/command/dispatch.js +343 -0
  26. package/psql/command/inputQueue.js +42 -0
  27. package/psql/command/shared.js +71 -0
  28. package/psql/complete/filenames.js +139 -0
  29. package/psql/complete/index.js +104 -0
  30. package/psql/complete/matcher.js +314 -0
  31. package/psql/complete/psqlVars.js +247 -0
  32. package/psql/complete/queries.js +491 -0
  33. package/psql/complete/rules.js +2387 -0
  34. package/psql/core/common.js +1250 -0
  35. package/psql/core/help.js +576 -0
  36. package/psql/core/mainloop.js +1353 -0
  37. package/psql/core/prompt.js +437 -0
  38. package/psql/core/settings.js +684 -0
  39. package/psql/core/sqlHelp.js +1066 -0
  40. package/psql/core/startup.js +840 -0
  41. package/psql/core/syncVars.js +116 -0
  42. package/psql/core/variables.js +287 -0
  43. package/psql/describe/formatters.js +1277 -0
  44. package/psql/describe/processNamePattern.js +270 -0
  45. package/psql/describe/queries.js +2373 -0
  46. package/psql/describe/versionGate.js +43 -0
  47. package/psql/index.js +2005 -0
  48. package/psql/io/history.js +299 -0
  49. package/psql/io/input.js +120 -0
  50. package/psql/io/lineEditor/buffer.js +323 -0
  51. package/psql/io/lineEditor/complete.js +227 -0
  52. package/psql/io/lineEditor/filename.js +159 -0
  53. package/psql/io/lineEditor/index.js +891 -0
  54. package/psql/io/lineEditor/keymap.js +738 -0
  55. package/psql/io/lineEditor/vt100.js +363 -0
  56. package/psql/io/pgpass.js +202 -0
  57. package/psql/io/pgservice.js +194 -0
  58. package/psql/io/psqlrc.js +422 -0
  59. package/psql/print/aligned.js +1756 -0
  60. package/psql/print/asciidoc.js +248 -0
  61. package/psql/print/crosstab.js +460 -0
  62. package/psql/print/csv.js +92 -0
  63. package/psql/print/html.js +258 -0
  64. package/psql/print/json.js +96 -0
  65. package/psql/print/latex.js +396 -0
  66. package/psql/print/pager.js +265 -0
  67. package/psql/print/troff.js +258 -0
  68. package/psql/print/unaligned.js +118 -0
  69. package/psql/print/units.js +135 -0
  70. package/psql/scanner/slash.js +513 -0
  71. package/psql/scanner/sql.js +910 -0
  72. package/psql/scanner/stringutils.js +390 -0
  73. package/psql/types/backslash.js +1 -0
  74. package/psql/types/connection.js +1 -0
  75. package/psql/types/index.js +7 -0
  76. package/psql/types/printer.js +1 -0
  77. package/psql/types/repl.js +1 -0
  78. package/psql/types/scanner.js +24 -0
  79. package/psql/types/settings.js +1 -0
  80. package/psql/types/variables.js +1 -0
  81. package/psql/wire/connection.js +2844 -0
  82. package/psql/wire/copy.js +108 -0
  83. package/psql/wire/notify.js +59 -0
  84. package/psql/wire/pipeline.js +519 -0
  85. package/psql/wire/protocol.js +466 -0
  86. package/psql/wire/sasl.js +296 -0
  87. package/psql/wire/tls.js +596 -0
  88. package/test_utils/fixtures.js +1 -0
  89. package/utils/esbuild.js +147 -0
  90. package/utils/psql.js +107 -11
  91. package/utils/zip.js +4 -0
  92. package/writer.js +1 -1
  93. package/commands/auth.test.js +0 -211
  94. package/commands/branches.test.js +0 -460
  95. package/commands/checkout.test.js +0 -170
  96. package/commands/connection_string.test.js +0 -196
  97. package/commands/data_api.test.js +0 -169
  98. package/commands/databases.test.js +0 -39
  99. package/commands/help.test.js +0 -9
  100. package/commands/init.test.js +0 -56
  101. package/commands/ip_allow.test.js +0 -59
  102. package/commands/link.test.js +0 -381
  103. package/commands/operations.test.js +0 -7
  104. package/commands/orgs.test.js +0 -7
  105. package/commands/projects.test.js +0 -144
  106. package/commands/psql.test.js +0 -49
  107. package/commands/roles.test.js +0 -37
  108. package/commands/set_context.test.js +0 -159
  109. package/commands/vpc_endpoints.test.js +0 -69
  110. package/context.test.js +0 -119
  111. package/env.test.js +0 -55
  112. package/utils/formats.test.js +0 -32
  113. 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.