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,299 @@
1
+ /**
2
+ * psql history file I/O.
3
+ *
4
+ * TypeScript port of the history-handling portion of PostgreSQL's
5
+ * `src/bin/psql/input.c`. Implements the read/append/truncate operations the
6
+ * REPL drives plus the small env-resolution helpers (`PSQL_HISTORY`,
7
+ * `HISTSIZE`).
8
+ *
9
+ * On-disk format follows GNU readline's history file:
10
+ *
11
+ * - One entry per line, UTF-8.
12
+ * - Multi-line entries are encoded with the literal escape sequences
13
+ * `\n` (backslash + n), `\r`, and `\\`. We decode them back to real
14
+ * newlines / carriage returns / backslashes on load and re-encode on
15
+ * write. Upstream psql uses an in-memory NL_IN_HISTORY (0x01) marker
16
+ * that libreadline then translates to the same on-disk form; we skip
17
+ * the intermediate marker and operate on real strings throughout.
18
+ * - Lines whose first character is `#` are treated as timestamp / comment
19
+ * markers (libreadline writes them when `history_write_timestamps` is
20
+ * set) and silently skipped on load. psql itself never writes them.
21
+ *
22
+ * Intentional deviations from upstream:
23
+ *
24
+ * - Append-only writes use `fs.promises.appendFile`, which opens the file
25
+ * with `O_APPEND` on POSIX so concurrent appends from multiple psql
26
+ * instances do not interleave within a single write call. On Windows
27
+ * there is a small race window we accept; the alternative (an external
28
+ * lock file) is not worth the dependency churn.
29
+ * - `truncateHistory` writes the trimmed history to a sibling temp file
30
+ * and `rename`s it into place. POSIX `rename(2)` is atomic w.r.t. other
31
+ * processes observing the path; Windows is best-effort.
32
+ * - HISTCONTROL comparisons run on the raw (decoded) entry — this matches
33
+ * bash/readline behaviour where the user's literal input is what gets
34
+ * deduped, not the escaped on-disk form.
35
+ */
36
+ import { promises as fs } from 'node:fs';
37
+ import * as os from 'node:os';
38
+ import * as path from 'node:path';
39
+ import { randomUUID } from 'node:crypto';
40
+ /** psql's compiled-in default for HISTSIZE (see `src/bin/psql/settings.h`). */
41
+ const DEFAULT_HISTSIZE = 500;
42
+ /**
43
+ * In-memory command-line history for the current session, oldest first.
44
+ *
45
+ * Upstream psql delegates history to GNU readline, whose `history_list()`
46
+ * backs the `\s` meta-command (`exec_command_s` in `command.c` prints the
47
+ * live in-memory history, NOT the on-disk file). We keep our own
48
+ * process-local mirror here, populated by {@link recordHistory} from the
49
+ * same funnel the REPL uses to persist a submitted line ({@link
50
+ * appendHistory}). `\s` reads it via {@link getHistory}.
51
+ *
52
+ * Module-level state matches the single-session lifetime of a psql process;
53
+ * tests reset it with {@link clearHistory}.
54
+ */
55
+ let inMemoryHistory = [];
56
+ /**
57
+ * Record one submitted line in the session's in-memory history.
58
+ *
59
+ * Empty lines and an exact duplicate of the immediately preceding entry are
60
+ * dropped, mirroring readline's default behaviour (and {@link
61
+ * LineEditor.pushHistory}); HISTCONTROL filtering is applied by the caller
62
+ * ({@link appendHistory}) before this is reached.
63
+ */
64
+ export const recordHistory = (entry) => {
65
+ if (entry.length === 0)
66
+ return;
67
+ if (inMemoryHistory[inMemoryHistory.length - 1] === entry)
68
+ return;
69
+ inMemoryHistory.push(entry);
70
+ };
71
+ /**
72
+ * Return the session's in-memory history (oldest first). Returns a copy so
73
+ * callers can't mutate the backing store.
74
+ */
75
+ export const getHistory = () => inMemoryHistory.slice();
76
+ /** Reset the in-memory history. Primarily for tests and `\s`-less restarts. */
77
+ export const clearHistory = () => {
78
+ inMemoryHistory = [];
79
+ };
80
+ /** Encode a single in-memory entry to the on-disk libreadline form. */
81
+ const encodeEntry = (entry) => entry.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
82
+ /**
83
+ * Decode a single on-disk libreadline line back to its in-memory form.
84
+ *
85
+ * Recognised escapes: `\\` → `\`, `\n` → newline, `\r` → CR. Any other
86
+ * `\<x>` sequence is left as-is (matching readline's lenient decoder), so
87
+ * a stray backslash at the end of the file does not eat the next entry.
88
+ */
89
+ const decodeEntry = (line) => {
90
+ let out = '';
91
+ for (let i = 0; i < line.length; i++) {
92
+ const c = line.charCodeAt(i);
93
+ if (c === 0x5c /* '\\' */ && i + 1 < line.length) {
94
+ const next = line[i + 1];
95
+ if (next === 'n') {
96
+ out += '\n';
97
+ i++;
98
+ continue;
99
+ }
100
+ if (next === 'r') {
101
+ out += '\r';
102
+ i++;
103
+ continue;
104
+ }
105
+ if (next === '\\') {
106
+ out += '\\';
107
+ i++;
108
+ continue;
109
+ }
110
+ }
111
+ out += line[i];
112
+ }
113
+ return out;
114
+ };
115
+ /**
116
+ * Return `true` if `entry` should be filtered out under `histcontrol`
117
+ * relative to the most recent entry already in history (`prev`).
118
+ *
119
+ * Mirrors `pg_send_history()`'s filter in `input.c`.
120
+ */
121
+ const shouldIgnore = (entry, prev, histcontrol) => {
122
+ const ignoreSpace = histcontrol === 'ignorespace' || histcontrol === 'ignoreboth';
123
+ const ignoreDups = histcontrol === 'ignoredups' || histcontrol === 'ignoreboth';
124
+ if (ignoreSpace && entry.length > 0 && /^\s/.test(entry))
125
+ return true;
126
+ if (ignoreDups && prev !== undefined && prev === entry)
127
+ return true;
128
+ return false;
129
+ };
130
+ /**
131
+ * Read a libreadline history file and return entries in chronological
132
+ * order (oldest first). A missing file resolves to `[]` so callers can
133
+ * unconditionally `loadHistory` at startup.
134
+ *
135
+ * Lines beginning with `#` are silently skipped (libreadline timestamp
136
+ * markers; psql itself never writes them but we tolerate them).
137
+ *
138
+ * Other I/O errors (EACCES, EISDIR, …) propagate to the caller.
139
+ */
140
+ export const loadHistory = async (filePath) => {
141
+ let raw;
142
+ try {
143
+ raw = await fs.readFile(filePath, 'utf8');
144
+ }
145
+ catch (err) {
146
+ if (err &&
147
+ typeof err === 'object' &&
148
+ 'code' in err &&
149
+ err.code === 'ENOENT') {
150
+ return [];
151
+ }
152
+ throw err;
153
+ }
154
+ // Strip a single trailing newline so a well-formed file ending in `\n`
155
+ // doesn't produce a phantom empty entry. Don't strip more than one — a
156
+ // blank line in the middle of the file represents an entry that was
157
+ // literally the empty string (rare but possible), and we round-trip it.
158
+ if (raw.endsWith('\n'))
159
+ raw = raw.slice(0, -1);
160
+ if (raw.length === 0)
161
+ return [];
162
+ const entries = [];
163
+ for (const line of raw.split('\n')) {
164
+ if (line.startsWith('#'))
165
+ continue;
166
+ entries.push(decodeEntry(line));
167
+ }
168
+ return entries;
169
+ };
170
+ /**
171
+ * Append a single entry to the history file, subject to HISTCONTROL.
172
+ *
173
+ * Implementation notes:
174
+ *
175
+ * - On POSIX, `fs.appendFile` opens with `O_APPEND`, which guarantees
176
+ * each `write(2)` lands at the current end-of-file. A single
177
+ * encoded entry plus its trailing `\n` is one write, so concurrent
178
+ * psql instances writing to the same history file never interleave
179
+ * within an entry. We accept a small race on Windows.
180
+ * - `ignoredups` consults the *last* entry currently on disk. We read
181
+ * the tail of the file rather than the whole history to keep this
182
+ * O(1)-ish for large histories; for simplicity we read everything
183
+ * when ignoredups is in effect — typical history files are well
184
+ * under 1 MiB.
185
+ */
186
+ export const appendHistory = async (filePath, entry, histcontrol = 'none') => {
187
+ if (histcontrol === 'ignoredups' ||
188
+ histcontrol === 'ignoreboth' ||
189
+ histcontrol === 'ignorespace') {
190
+ let prev;
191
+ if (histcontrol === 'ignoredups' || histcontrol === 'ignoreboth') {
192
+ const existing = await loadHistory(filePath);
193
+ prev = existing[existing.length - 1];
194
+ }
195
+ if (shouldIgnore(entry, prev, histcontrol))
196
+ return;
197
+ }
198
+ // Mirror the line into the session's in-memory history (what `\s` prints)
199
+ // using the same funnel that persists it to disk, so the two stay in
200
+ // lock-step under HISTCONTROL. Recording happens AFTER the ignore check
201
+ // so an ignored line is absent from both.
202
+ recordHistory(entry);
203
+ await fs.appendFile(filePath, encodeEntry(entry) + '\n', 'utf8');
204
+ };
205
+ /**
206
+ * Trim the history file to its last `maxLines` entries.
207
+ *
208
+ * If `maxLines <= 0` the file is removed entirely (matching psql's
209
+ * behaviour when HISTSIZE is set to 0). If the file already fits the
210
+ * cap, it's left untouched. Otherwise we write the kept tail to a
211
+ * sibling temp file and `rename` it into place.
212
+ *
213
+ * `rename(2)` on POSIX is atomic w.r.t. observers of the destination
214
+ * path; on Windows the platform call may briefly fail if another
215
+ * process has the destination open, in which case the error propagates.
216
+ */
217
+ export const truncateHistory = async (filePath, maxLines) => {
218
+ if (maxLines <= 0) {
219
+ try {
220
+ await fs.unlink(filePath);
221
+ }
222
+ catch (err) {
223
+ if (err &&
224
+ typeof err === 'object' &&
225
+ 'code' in err &&
226
+ err.code === 'ENOENT') {
227
+ return;
228
+ }
229
+ throw err;
230
+ }
231
+ return;
232
+ }
233
+ const entries = await loadHistory(filePath);
234
+ if (entries.length <= maxLines)
235
+ return;
236
+ const kept = entries.slice(entries.length - maxLines);
237
+ const body = kept.map(encodeEntry).join('\n') + '\n';
238
+ const dir = path.dirname(filePath);
239
+ const base = path.basename(filePath);
240
+ const tmpPath = path.join(dir, `.${base}.${randomUUID()}.tmp`);
241
+ await fs.writeFile(tmpPath, body, { encoding: 'utf8', mode: 0o600 });
242
+ try {
243
+ await fs.rename(tmpPath, filePath);
244
+ }
245
+ catch (err) {
246
+ // Best-effort cleanup; the temp file is harmless but noisy.
247
+ try {
248
+ await fs.unlink(tmpPath);
249
+ }
250
+ catch {
251
+ /* ignore */
252
+ }
253
+ throw err;
254
+ }
255
+ };
256
+ /**
257
+ * Resolve the on-disk history file path psql would use, in order of
258
+ * precedence:
259
+ *
260
+ * 1. `$PSQL_HISTORY` if set and non-empty.
261
+ * 2. On Windows: `%APPDATA%\postgresql\psql_history`.
262
+ * 3. Otherwise: `$HOME/.psql_history` (falling back to `os.homedir()`
263
+ * if `$HOME` is unset, e.g. in some sandboxes).
264
+ *
265
+ * Pure function — `env` defaults to `process.env` but can be injected
266
+ * for tests.
267
+ */
268
+ export const defaultHistoryPath = (env = process.env) => {
269
+ const explicit = env.PSQL_HISTORY;
270
+ if (explicit !== undefined && explicit.length > 0)
271
+ return explicit;
272
+ if (process.platform === 'win32') {
273
+ const appdata = env.APPDATA;
274
+ if (appdata !== undefined && appdata.length > 0) {
275
+ return path.join(appdata, 'postgresql', 'psql_history');
276
+ }
277
+ // Fall through to homedir() if APPDATA isn't set; matches psql's
278
+ // graceful degradation on a minimally-configured Windows session.
279
+ }
280
+ const home = env.HOME ?? os.homedir();
281
+ return path.join(home, '.psql_history');
282
+ };
283
+ /**
284
+ * Resolve the effective HISTSIZE: the `HISTSIZE` env var (if a
285
+ * non-negative integer) or psql's compiled-in default of 500.
286
+ *
287
+ * Values that fail to parse as a non-negative integer fall back to the
288
+ * default — psql itself ignores malformed HISTSIZE and warns, but at
289
+ * this layer we don't have a logger.
290
+ */
291
+ export const resolveHistSize = (env = process.env) => {
292
+ const raw = env.HISTSIZE;
293
+ if (raw === undefined || raw === '')
294
+ return DEFAULT_HISTSIZE;
295
+ const n = Number(raw);
296
+ if (!Number.isInteger(n) || n < 0)
297
+ return DEFAULT_HISTSIZE;
298
+ return n;
299
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Interactive line input for the psql REPL — the shared "read one line from
3
+ * the user" primitive used by `\prompt` and `\password`.
4
+ *
5
+ * Models upstream psql's password / prompt reading:
6
+ * - `simple_prompt` (`src/port/sprompt.c`) reads a line with optional echo
7
+ * suppression. On a TTY it disables terminal echo for the duration of the
8
+ * read (we emulate this with Node's raw mode + manual character handling);
9
+ * when stdin is NOT a TTY it falls back to a plain line read and the
10
+ * `echo` flag is a no-op — the line is still consumed. We match that: a
11
+ * non-interactive caller piping a password in still has it read.
12
+ * - `\prompt` (`exec_command_prompt`) reads with echo on; `\prompt -`
13
+ * (no-echo password form) and `\password` read with echo suppressed.
14
+ *
15
+ * The terminal handling is parameterised over the input/output streams so the
16
+ * non-TTY path (the only one testable without a real PTY) can be exercised in
17
+ * unit tests; production callers use {@link readLine} with the process
18
+ * defaults.
19
+ */
20
+ import { createInterface } from 'node:readline';
21
+ /** True when `stream` is a TTY whose echo we can suppress via raw mode. */
22
+ const isRawCapableTty = (stream) => {
23
+ const s = stream;
24
+ return Boolean(s.isTTY) && typeof s.setRawMode === 'function';
25
+ };
26
+ /**
27
+ * Read one line of input, optionally suppressing echo.
28
+ *
29
+ * - `echo: false` on a TTY → put the terminal in raw mode, echo nothing,
30
+ * accumulate characters until Enter, then restore cooked mode. Used for
31
+ * password entry (`\password`, `\prompt -`).
32
+ * - `echo: true`, or any non-TTY input → a plain line read. On a non-TTY the
33
+ * `echo` flag is a no-op: we still consume and return the line, matching
34
+ * upstream `simple_prompt`'s behaviour with redirected stdin.
35
+ *
36
+ * The returned string excludes the trailing newline. EOF before any newline
37
+ * resolves to whatever was typed so far (empty string if nothing).
38
+ */
39
+ export const readLine = (prompt, opts) => {
40
+ const input = opts.input ?? process.stdin;
41
+ const output = opts.output ?? process.stderr;
42
+ if (!opts.echo && isRawCapableTty(input)) {
43
+ return readNoEchoTty(prompt, input, output);
44
+ }
45
+ return readEchoLine(prompt, input, output);
46
+ };
47
+ /** Plain, echoing (or non-TTY) line read via `node:readline`. */
48
+ const readEchoLine = (prompt, input, output) => {
49
+ const rl = createInterface({ input, output, terminal: false });
50
+ return new Promise((resolve) => {
51
+ let settled = false;
52
+ const settle = (line) => {
53
+ if (settled)
54
+ return;
55
+ settled = true;
56
+ rl.close();
57
+ resolve(line);
58
+ };
59
+ if (prompt.length > 0)
60
+ output.write(prompt);
61
+ // Resolve on the first complete line. Closing without one (EOF) yields ''.
62
+ // We don't rely on `line` firing before `close`: whichever lands first
63
+ // wins, and a buffered final line is delivered as a `line` event.
64
+ rl.on('line', (l) => {
65
+ settle(l);
66
+ });
67
+ rl.on('close', () => {
68
+ settle('');
69
+ });
70
+ });
71
+ };
72
+ /**
73
+ * No-echo read on a raw-capable TTY. Mirrors upstream `simple_prompt`'s
74
+ * echo-off branch: switch to raw mode, gather bytes, never echo them, and
75
+ * restore on Enter / EOF / interrupt. Backspace edits the buffer; Ctrl-C and
76
+ * Ctrl-D abort with whatever has been typed (empty on a clean Ctrl-C).
77
+ */
78
+ const readNoEchoTty = (prompt, input, output) => {
79
+ return new Promise((resolve) => {
80
+ if (prompt.length > 0)
81
+ output.write(prompt);
82
+ input.setRawMode(true);
83
+ input.resume();
84
+ input.setEncoding('utf8');
85
+ let buf = '';
86
+ const finish = (result) => {
87
+ input.setRawMode(false);
88
+ input.pause();
89
+ input.removeListener('data', onData);
90
+ // Terminate the (un-echoed) line the user couldn't see themselves type.
91
+ output.write('\n');
92
+ resolve(result);
93
+ };
94
+ const onData = (chunk) => {
95
+ for (const ch of chunk) {
96
+ if (ch === '\n' || ch === '\r') {
97
+ finish(buf);
98
+ return;
99
+ }
100
+ if (ch === '') {
101
+ // Ctrl-C: cancel, return nothing.
102
+ finish('');
103
+ return;
104
+ }
105
+ if (ch === '') {
106
+ // Ctrl-D (EOF): return what we have.
107
+ finish(buf);
108
+ return;
109
+ }
110
+ if (ch === '' || ch === '\b') {
111
+ // DEL / Backspace: drop the last character.
112
+ buf = buf.slice(0, -1);
113
+ continue;
114
+ }
115
+ buf += ch;
116
+ }
117
+ };
118
+ input.on('data', onData);
119
+ });
120
+ };