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,909 @@
1
+ /**
2
+ * Formatting backslash commands.
3
+ *
4
+ * TypeScript port of the `exec_command_a/C/f/H/t/T/x/pset/encoding`
5
+ * implementations in upstream PostgreSQL's `src/bin/psql/command.c` and
6
+ * their backing `do_pset()` / `printPsetInfo()` helpers in the same file.
7
+ *
8
+ * All commands mutate `settings.popt.topt` (a {@link PrintTableOpts}) in
9
+ * place. Several are thin wrappers over `\pset <option>` — for instance
10
+ * `\a` is equivalent to `\pset format aligned|unaligned`. We keep the
11
+ * separate exports so the registry can advertise them under their public
12
+ * names without aliasing oddities.
13
+ *
14
+ * Encoding & connection coupling: `\encoding NAME` propagates the client
15
+ * encoding to the live connection via {@link Connection.setClientEncoding}
16
+ * (libpq's `PQsetClientEncoding`). Following upstream `do_encoding`, we
17
+ * validate the name client-side first — an unrecognised name prints
18
+ * `invalid encoding name "<name>"` and leaves the encoding unchanged,
19
+ * never touching the connection.
20
+ *
21
+ * Error format: upstream uses `\\<cmd>: <message>` for diagnostics. We
22
+ * mirror that exactly, writing to stderr and returning
23
+ * `{ status: 'error' }`.
24
+ */
25
+ import { scanSlashArgs } from '../scanner/slash.js';
26
+ import { writeErr, writeOut, parseBool, parseTriple } from './shared.js';
27
+ /** Recognised output-format names accepted by `\pset format`. */
28
+ const OUTPUT_FORMATS = [
29
+ 'aligned',
30
+ 'unaligned',
31
+ 'wrapped',
32
+ 'html',
33
+ 'asciidoc',
34
+ 'latex',
35
+ 'latex-longtable',
36
+ 'troff-ms',
37
+ 'csv',
38
+ 'json',
39
+ ];
40
+ /** Convert OutputFormat to its human-readable display string. */
41
+ const formatName = (f) => f;
42
+ /**
43
+ * Stringify a triple-state for status lines (`\x`, `\pset expanded`).
44
+ * Matches upstream psql phrasing:
45
+ * on → "Expanded display is on."
46
+ * off → "Expanded display is off."
47
+ * auto → "Expanded display is used automatically."
48
+ */
49
+ const tripleLabel = (value) => value === 'auto' ? 'used automatically' : value;
50
+ /**
51
+ * `\a` — toggle aligned/unaligned.
52
+ *
53
+ * Upstream `exec_command_a` flips the format via `do_pset("format", …)`
54
+ * and, when not quiet, prints `Output format is <aligned|unaligned>.` —
55
+ * the same `printPsetInfo("format")` line `\pset format` emits. We mirror
56
+ * that here, gating the status write on QUIET like the sibling toggles.
57
+ */
58
+ export const cmdA = {
59
+ name: 'a',
60
+ helpKey: 'a',
61
+ run: (ctx) => {
62
+ const topt = ctx.settings.popt.topt;
63
+ topt.format = topt.format === 'aligned' ? 'unaligned' : 'aligned';
64
+ if (!ctx.settings.quiet) {
65
+ writeOut(`Output format is ${formatName(topt.format)}.\n`);
66
+ }
67
+ return Promise.resolve({ status: 'ok' });
68
+ },
69
+ };
70
+ /**
71
+ * `\C [title]` — set or clear `topt.title`. No arg clears, any arg sets to
72
+ * that string verbatim. Equivalent to `\pset title [value]`; upstream
73
+ * `exec_command_C` dispatches via `do_pset("title", value, …)` so the
74
+ * status line (`Title is "…".` / `Title is unset.`) is emitted by
75
+ * `printPsetInfo`.
76
+ */
77
+ export const cmdC = {
78
+ name: 'C',
79
+ helpKey: 'C',
80
+ run: (ctx) => {
81
+ const arg = ctx.nextArg('normal');
82
+ return Promise.resolve(applyPset(ctx.settings.popt.topt, 'title', arg, ctx.cmdName, ctx.settings.quiet));
83
+ },
84
+ };
85
+ /**
86
+ * `\f [sep]` — set or show the unaligned field separator. With no arg, we
87
+ * print the current value (upstream prints `Field separator is "%s".`).
88
+ */
89
+ export const cmdF = {
90
+ name: 'f',
91
+ helpKey: 'f',
92
+ run: (ctx) => {
93
+ const arg = ctx.nextArg('normal');
94
+ if (arg === null) {
95
+ writeOut(`Field separator is "${ctx.settings.popt.topt.fieldSep}".\n`);
96
+ return Promise.resolve({ status: 'ok' });
97
+ }
98
+ ctx.settings.popt.topt.fieldSep = arg;
99
+ // Upstream confirms the change (quiet-gated), like the other \pset-style
100
+ // setters (review: `\f SEP` is silent).
101
+ if (!ctx.settings.quiet) {
102
+ writeOut(`Field separator is "${arg}".\n`);
103
+ }
104
+ return Promise.resolve({ status: 'ok' });
105
+ },
106
+ };
107
+ /**
108
+ * `\H` — toggle html on/off. If currently `html`, flip back to `aligned`;
109
+ * otherwise flip to `html` (upstream remembers the prior format only
110
+ * loosely — we always restore `aligned` to match the documented behaviour).
111
+ */
112
+ export const cmdH = {
113
+ name: 'H',
114
+ helpKey: 'H',
115
+ run: (ctx) => {
116
+ const topt = ctx.settings.popt.topt;
117
+ topt.format = topt.format === 'html' ? 'aligned' : 'html';
118
+ return Promise.resolve({ status: 'ok' });
119
+ },
120
+ };
121
+ /**
122
+ * `\t [on|off|toggle]` — tuples-only. No arg → toggle.
123
+ *
124
+ * Equivalent to `\pset tuples_only [value]`; upstream `exec_command_t`
125
+ * dispatches via `do_pset("tuples_only", opt, …)`. The
126
+ * `printPsetInfo("tuples_only")` confirmation line is only emitted when
127
+ * `opt` is NULL (the toggle path) — when a value is supplied,
128
+ * `do_pset` returns early via `ParseVariableBool` and skips
129
+ * `printPsetInfo`, so the status line is suppressed.
130
+ */
131
+ export const cmdT = {
132
+ name: 't',
133
+ helpKey: 't',
134
+ run: (ctx) => {
135
+ const arg = ctx.nextArg('normal');
136
+ return Promise.resolve(applyPset(ctx.settings.popt.topt, 'tuples_only', arg, ctx.cmdName, ctx.settings.quiet));
137
+ },
138
+ };
139
+ /**
140
+ * `\T [attr]` — set HTML table attributes. No arg clears. Equivalent to
141
+ * `\pset tableattr [value]`; upstream `exec_command_T` dispatches via
142
+ * `do_pset("tableattr", value, …)` so the status line
143
+ * (`Table attributes are "…".` / `Table attributes unset.`) is emitted
144
+ * by `printPsetInfo`.
145
+ */
146
+ export const cmdTitleAttr = {
147
+ name: 'T',
148
+ helpKey: 'T',
149
+ run: (ctx) => {
150
+ const arg = ctx.nextArg('normal');
151
+ return Promise.resolve(applyPset(ctx.settings.popt.topt, 'tableattr', arg, ctx.cmdName, ctx.settings.quiet));
152
+ },
153
+ };
154
+ /** `\x [on|off|auto|toggle]` — expanded output. No arg → toggle. */
155
+ export const cmdX = {
156
+ name: 'x',
157
+ helpKey: 'x',
158
+ run: (ctx) => {
159
+ const arg = ctx.nextArg('normal');
160
+ const topt = ctx.settings.popt.topt;
161
+ let next;
162
+ if (arg === null) {
163
+ next = topt.expanded === 'on' ? 'off' : 'on';
164
+ }
165
+ else {
166
+ const parsed = parseTriple(arg);
167
+ if (parsed === null) {
168
+ writeErr(`\\${ctx.cmdName}: unrecognized value "${arg}": Boolean expected\n`);
169
+ return Promise.resolve({ status: 'error', errorWritten: true });
170
+ }
171
+ if (parsed === 'toggle') {
172
+ next = topt.expanded === 'on' ? 'off' : 'on';
173
+ }
174
+ else {
175
+ next = parsed;
176
+ }
177
+ }
178
+ topt.expanded = next;
179
+ writeOut(`Expanded display is ${tripleLabel(next)}.\n`);
180
+ return Promise.resolve({ status: 'ok' });
181
+ },
182
+ };
183
+ /**
184
+ * Canonical PostgreSQL encoding names, normalised the way upstream
185
+ * `pg_char_to_encoding` (`src/common/encnames.c`) normalises before
186
+ * comparing: lowercased with every `-` and `_` removed. The set covers the
187
+ * full `pg_enc2name` table — both the backend-usable encodings and the
188
+ * client-only ones (SJIS, BIG5, …) — because upstream `\encoding` validates
189
+ * via `pg_char_to_encoding`, which accepts any recognised name and lets the
190
+ * server reject genuinely unusable ones.
191
+ */
192
+ const NORMALISED_ENCODINGS = new Set([
193
+ 'sqlascii',
194
+ 'eucjp',
195
+ 'euccn',
196
+ 'euckr',
197
+ 'euctw',
198
+ 'eucjis2004',
199
+ 'utf8',
200
+ 'muleinternal',
201
+ 'latin1',
202
+ 'latin2',
203
+ 'latin3',
204
+ 'latin4',
205
+ 'latin5',
206
+ 'latin6',
207
+ 'latin7',
208
+ 'latin8',
209
+ 'latin9',
210
+ 'latin10',
211
+ 'win1256',
212
+ 'win1258',
213
+ 'win866',
214
+ 'win874',
215
+ 'koi8r',
216
+ 'win1251',
217
+ 'win1252',
218
+ 'iso88595',
219
+ 'iso88596',
220
+ 'iso88597',
221
+ 'iso88598',
222
+ 'win1250',
223
+ 'win1253',
224
+ 'win1254',
225
+ 'win1255',
226
+ 'win1257',
227
+ 'koi8u',
228
+ 'sjis',
229
+ 'big5',
230
+ 'gbk',
231
+ 'uhc',
232
+ 'gb18030',
233
+ 'johab',
234
+ 'shiftjis2004',
235
+ // Aliases upstream's encoding_match_list accepts as recognised names.
236
+ 'unicode',
237
+ 'mskanji',
238
+ 'shiftjis',
239
+ 'windows949',
240
+ 'windows950',
241
+ 'windows936',
242
+ 'tcvn',
243
+ 'tcvn5712',
244
+ 'vscii',
245
+ 'alt',
246
+ 'win',
247
+ 'koi8',
248
+ 'abc', // → WIN1258
249
+ ]);
250
+ /**
251
+ * Mirror upstream `pg_char_to_encoding`'s name normalisation: drop every
252
+ * `-` and `_`, lowercase, and look the result up in the canonical set.
253
+ * Used to reproduce `\encoding`'s `invalid encoding name` guard without a
254
+ * server round-trip.
255
+ */
256
+ const isValidEncodingName = (name) => NORMALISED_ENCODINGS.has(name.replace(/[-_]/g, '').toLowerCase());
257
+ /**
258
+ * `\encoding [name]` — show or set the client encoding.
259
+ *
260
+ * No arg: print the current `topt.encoding`. With an arg: validate the name
261
+ * the way upstream `do_encoding` does (`pg_char_to_encoding` >= 0) — an
262
+ * unrecognised name prints `\encoding: invalid encoding name "<name>"`,
263
+ * leaves `topt.encoding` untouched, and never calls the connection.
264
+ * Otherwise push it to the live connection via
265
+ * {@link Connection.setClientEncoding} (libpq `PQsetClientEncoding`) and
266
+ * mirror it into `topt.encoding` so prompts/printer see the new value.
267
+ */
268
+ export const cmdEncoding = {
269
+ name: 'encoding',
270
+ helpKey: 'encoding',
271
+ run: async (ctx) => {
272
+ const arg = ctx.nextArg('normal');
273
+ if (arg === null) {
274
+ writeOut(`${ctx.settings.popt.topt.encoding}\n`);
275
+ return { status: 'ok' };
276
+ }
277
+ if (!isValidEncodingName(arg)) {
278
+ // Upstream `do_encoding` reports the rejected name verbatim and leaves
279
+ // the current encoding in place.
280
+ writeErr(`\\${ctx.cmdName}: invalid encoding name "${arg}"\n`);
281
+ return { status: 'error', errorWritten: true };
282
+ }
283
+ const { db } = ctx.settings;
284
+ // `PgConnection` always implements setClientEncoding; the optional call
285
+ // only short-circuits when there's no live connection (or a partial mock
286
+ // in tests), in which case we still mirror the encoding into topt below.
287
+ if (db?.setClientEncoding) {
288
+ try {
289
+ await db.setClientEncoding(arg);
290
+ }
291
+ catch (err) {
292
+ // Server refused the SET (or the connection failed mid-flight);
293
+ // surface the diagnostic and leave topt.encoding unchanged, matching
294
+ // upstream's "leave the encoding as it was on failure" behaviour.
295
+ const msg = err instanceof Error ? err.message : String(err);
296
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
297
+ return { status: 'error', errorWritten: true };
298
+ }
299
+ }
300
+ ctx.settings.popt.topt.encoding = arg;
301
+ return { status: 'ok' };
302
+ },
303
+ };
304
+ /**
305
+ * The heart of `\pset`. Given a parsed `option` and optional `value`,
306
+ * mutates `topt` in place and emits the upstream-style status line.
307
+ *
308
+ * Returns `{ status: 'error' }` and writes an error if the value is
309
+ * unrecognised; `{ status: 'ok' }` otherwise.
310
+ *
311
+ * Wording reference: every status line is byte-matched against the
312
+ * `printPsetInfo` table in upstream `src/bin/psql/command.c`. Notable
313
+ * subtleties:
314
+ *
315
+ * - `tuples_only`, `footer`, and `numericlocale` are silenced when a
316
+ * value is supplied — upstream `do_pset` returns directly out of
317
+ * `ParseVariableBool`, never reaching `printPsetInfo`. The toggle
318
+ * paths still print.
319
+ * - `recordsep` renders the literal `\n` as the `<newline>` sentinel.
320
+ * - `columns` reports `0` as `Target width is unset.`.
321
+ * - `unicode_*_linestyle` uses the multi-word "line style" phrasing
322
+ * even though the option name itself is a single token.
323
+ * - `pager_min_lines` pluralizes via `ngettext` — singular at 1, plural
324
+ * everywhere else (including 0).
325
+ * - `csv_fieldsep` reports as `Field separator for CSV is "…".`.
326
+ * - `xheader_width` quotes the named enum values (`"full"`/`"column"`/
327
+ * `"page"`) and prints the numeric form unquoted.
328
+ */
329
+ export const applyPset = (topt, option, value, cmdName,
330
+ // When `silent` is true, suppress the "X is now Y." status lines that
331
+ // `\pset` emits on a successful set. Errors (invalid option / bad
332
+ // value) still go to stderr. Used by `\g (option=value ...)` —
333
+ // upstream applies the temporary overrides silently.
334
+ silent = false) => {
335
+ const writeOutMaybe = silent ? () => undefined : writeOut;
336
+ const opt = option.toLowerCase();
337
+ switch (opt) {
338
+ case 'format': {
339
+ if (value === null) {
340
+ writeOutMaybe(`Output format is ${formatName(topt.format)}.\n`);
341
+ return { status: 'ok' };
342
+ }
343
+ const v = value.toLowerCase();
344
+ // Upstream `do_pset` accepts unambiguous prefix matches for the
345
+ // format name via a hand-rolled cascade of `pg_strncasecmp` checks:
346
+ //
347
+ // 1. Special ambiguity guard for "aligned" vs "asciidoc" — any
348
+ // input that is a prefix of BOTH is rejected with the
349
+ // "ambiguous abbreviation" diagnostic (the only pair where
350
+ // upstream cares).
351
+ // 2. Otherwise pick the first OUTPUT_FORMATS entry that starts
352
+ // with `v` (the order in OUTPUT_FORMATS — `latex` before
353
+ // `latex-longtable` etc. — encodes which canonical name wins
354
+ // when one is a prefix of another).
355
+ const startsWithBoth = 'aligned'.startsWith(v) && 'asciidoc'.startsWith(v);
356
+ if (startsWithBoth) {
357
+ writeErr(`\\pset: ambiguous abbreviation "${value}" matches both "aligned" and "asciidoc"\n`);
358
+ return { status: 'error', errorWritten: true };
359
+ }
360
+ const match = OUTPUT_FORMATS.find((f) => f.startsWith(v));
361
+ if (!match) {
362
+ writeErr(`\\pset: allowed formats are aligned, asciidoc, csv, html, json, latex, latex-longtable, troff-ms, unaligned, wrapped\n`);
363
+ return { status: 'error', errorWritten: true };
364
+ }
365
+ topt.format = match;
366
+ writeOutMaybe(`Output format is ${formatName(match)}.\n`);
367
+ return { status: 'ok' };
368
+ }
369
+ case 'border': {
370
+ if (value === null) {
371
+ writeOutMaybe(`Border style is ${topt.border}.\n`);
372
+ return { status: 'ok' };
373
+ }
374
+ const n = parseInt(value, 10);
375
+ if (!Number.isFinite(n) || n < 0 || n > 3) {
376
+ writeErr(`\\pset: invalid border "${value}"\n`);
377
+ return { status: 'error', errorWritten: true };
378
+ }
379
+ topt.border = n;
380
+ writeOutMaybe(`Border style is ${topt.border}.\n`);
381
+ return { status: 'ok' };
382
+ }
383
+ case 'expanded':
384
+ case 'x': {
385
+ if (value === null) {
386
+ topt.expanded = topt.expanded === 'on' ? 'off' : 'on';
387
+ }
388
+ else {
389
+ const p = parseTriple(value);
390
+ if (p === null) {
391
+ writeErr(`\\pset: unrecognized value "${value}" for "expanded": Boolean expected\n`);
392
+ return { status: 'error', errorWritten: true };
393
+ }
394
+ topt.expanded =
395
+ p === 'toggle' ? (topt.expanded === 'on' ? 'off' : 'on') : p;
396
+ }
397
+ writeOutMaybe(`Expanded display is ${tripleLabel(topt.expanded)}.\n`);
398
+ return { status: 'ok' };
399
+ }
400
+ case 'fieldsep': {
401
+ if (value === null) {
402
+ writeOutMaybe(`Field separator is "${topt.fieldSep}".\n`);
403
+ return { status: 'ok' };
404
+ }
405
+ topt.fieldSep = value;
406
+ writeOutMaybe(`Field separator is "${topt.fieldSep}".\n`);
407
+ return { status: 'ok' };
408
+ }
409
+ case 'fieldsep_zero': {
410
+ // Upstream: any value (or none) forces fieldSep to the NUL byte.
411
+ // The bulk-view's `fieldsep_zero` line is derived from fieldSep
412
+ // (on iff fieldSep === '\0').
413
+ topt.fieldSep = '\0';
414
+ writeOutMaybe('Field separator is zero byte.\n');
415
+ return { status: 'ok' };
416
+ }
417
+ case 'footer': {
418
+ if (value !== null) {
419
+ // Upstream `do_pset` returns directly from `ParseVariableBool`
420
+ // for `footer`, bypassing the `printPsetInfo` call entirely
421
+ // — `\pset footer on` is silent, while `\pset footer`
422
+ // (toggle) still prints the new state.
423
+ const b = parseBool(value);
424
+ if (b === null) {
425
+ writeErr(`\\pset: unrecognized value "${value}" for "footer": Boolean expected\n`);
426
+ return { status: 'error', errorWritten: true };
427
+ }
428
+ topt.defaultFooter = b;
429
+ return { status: 'ok' };
430
+ }
431
+ topt.defaultFooter = !topt.defaultFooter;
432
+ writeOutMaybe(topt.defaultFooter
433
+ ? 'Default footer is on.\n'
434
+ : 'Default footer is off.\n');
435
+ return { status: 'ok' };
436
+ }
437
+ case 'recordsep': {
438
+ if (value !== null) {
439
+ topt.recordSep = value;
440
+ }
441
+ // Upstream `printPsetInfo` has three branches: the separator-zero
442
+ // path (handled by the dedicated `recordsep_zero` case), the
443
+ // "<newline>" sentinel for the literal `\n` byte, and the quoted
444
+ // verbatim form for everything else.
445
+ if (topt.recordSep === '\n') {
446
+ writeOutMaybe('Record separator is <newline>.\n');
447
+ }
448
+ else {
449
+ writeOutMaybe(`Record separator is "${topt.recordSep}".\n`);
450
+ }
451
+ return { status: 'ok' };
452
+ }
453
+ case 'recordsep_zero': {
454
+ topt.recordSep = '\0';
455
+ writeOutMaybe('Record separator is zero byte.\n');
456
+ return { status: 'ok' };
457
+ }
458
+ case 'tuples_only':
459
+ case 't': {
460
+ if (value !== null) {
461
+ // Upstream `do_pset` returns directly from `ParseVariableBool`
462
+ // for `tuples_only`, bypassing `printPsetInfo` — so
463
+ // `\pset tuples_only on` (and the equivalent `\t on`) is
464
+ // silent. The toggle path (no value) still prints.
465
+ const b = parseBool(value);
466
+ if (b === null) {
467
+ writeErr(`\\pset: unrecognized value "${value}": Boolean expected\n`);
468
+ return { status: 'error', errorWritten: true };
469
+ }
470
+ topt.tuplesOnly = b;
471
+ return { status: 'ok' };
472
+ }
473
+ topt.tuplesOnly = !topt.tuplesOnly;
474
+ writeOutMaybe(topt.tuplesOnly ? 'Tuples only is on.\n' : 'Tuples only is off.\n');
475
+ return { status: 'ok' };
476
+ }
477
+ case 'title': {
478
+ topt.title = value;
479
+ if (value === null) {
480
+ writeOutMaybe('Title is unset.\n');
481
+ }
482
+ else {
483
+ writeOutMaybe(`Title is "${value}".\n`);
484
+ }
485
+ return { status: 'ok' };
486
+ }
487
+ case 'tableattr':
488
+ case 't_a': {
489
+ topt.tableAttr = value;
490
+ if (value === null) {
491
+ writeOutMaybe('Table attributes unset.\n');
492
+ }
493
+ else {
494
+ writeOutMaybe(`Table attributes are "${value}".\n`);
495
+ }
496
+ return { status: 'ok' };
497
+ }
498
+ case 'pager': {
499
+ if (value === null) {
500
+ topt.pager = topt.pager === 'off' ? 'on' : 'off';
501
+ }
502
+ else {
503
+ const lower = value.toLowerCase();
504
+ if (lower === 'always') {
505
+ topt.pager = 'always';
506
+ }
507
+ else if (lower === 'on' || lower === 'off') {
508
+ topt.pager = lower;
509
+ }
510
+ else {
511
+ const b = parseBool(value);
512
+ if (b === null) {
513
+ writeErr(`\\pset: unrecognized value "${value}" for "pager"\n`);
514
+ return { status: 'error', errorWritten: true };
515
+ }
516
+ topt.pager = b ? 'on' : 'off';
517
+ }
518
+ }
519
+ writeOutMaybe(topt.pager === 'always'
520
+ ? 'Pager is always used.\n'
521
+ : topt.pager === 'on'
522
+ ? 'Pager is used for long output.\n'
523
+ : 'Pager usage is off.\n');
524
+ return { status: 'ok' };
525
+ }
526
+ case 'pager_min_lines': {
527
+ if (value !== null) {
528
+ const n = parseInt(value, 10);
529
+ if (!Number.isFinite(n) || n < 0) {
530
+ writeErr(`\\pset: invalid pager_min_lines "${value}"\n`);
531
+ return { status: 'error', errorWritten: true };
532
+ }
533
+ topt.pagerMinLines = n;
534
+ }
535
+ // Upstream uses `ngettext` so singular ("line") fires only for
536
+ // n == 1; 0 and 2+ render as "lines".
537
+ const lines = topt.pagerMinLines;
538
+ const unit = lines === 1 ? 'line' : 'lines';
539
+ writeOutMaybe(`Pager won't be used for less than ${lines} ${unit}.\n`);
540
+ return { status: 'ok' };
541
+ }
542
+ case 'null': {
543
+ topt.nullPrint = value ?? '';
544
+ writeOutMaybe(`Null display is "${topt.nullPrint}".\n`);
545
+ return { status: 'ok' };
546
+ }
547
+ case 'csv_fieldsep': {
548
+ if (value !== null) {
549
+ // Upstream `do_pset` splits the validation in two: length-based
550
+ // ("must be a single one-byte character") fires for empty / multi-
551
+ // char inputs *and* for the NUL byte (because in C the string is
552
+ // NUL-terminated, so `'\0'` decodes to an empty C string). The
553
+ // "cannot be a double quote, a newline, or a carriage return"
554
+ // path is reserved for the three forbidden single-byte values.
555
+ if (value.length !== 1 || value === '\0') {
556
+ writeErr(`\\pset: csv_fieldsep must be a single one-byte character\n`);
557
+ return { status: 'error', errorWritten: true };
558
+ }
559
+ if (value === '"' || value === '\n' || value === '\r') {
560
+ writeErr(`\\pset: csv_fieldsep cannot be a double quote, a newline, or a carriage return\n`);
561
+ return { status: 'error', errorWritten: true };
562
+ }
563
+ topt.csvFieldSep = value;
564
+ }
565
+ // Upstream wording: "Field separator for CSV is "%s".".
566
+ writeOutMaybe(`Field separator for CSV is "${topt.csvFieldSep}".\n`);
567
+ return { status: 'ok' };
568
+ }
569
+ case 'numericlocale': {
570
+ if (value !== null) {
571
+ // Upstream `do_pset` returns directly from `ParseVariableBool`
572
+ // for `numericlocale`, bypassing `printPsetInfo`. The toggle
573
+ // path (no value) still prints the new state.
574
+ const p = parseTriple(value);
575
+ if (p === null || p === 'auto') {
576
+ writeErr(`\\pset: unrecognized value "${value}" for "numericlocale": Boolean expected\n`);
577
+ return { status: 'error', errorWritten: true };
578
+ }
579
+ topt.numericLocale = p === 'toggle' ? !topt.numericLocale : p === 'on';
580
+ return { status: 'ok' };
581
+ }
582
+ topt.numericLocale = !topt.numericLocale;
583
+ writeOutMaybe(topt.numericLocale
584
+ ? 'Locale-adjusted numeric output is on.\n'
585
+ : 'Locale-adjusted numeric output is off.\n');
586
+ return { status: 'ok' };
587
+ }
588
+ case 'linestyle': {
589
+ if (value === null) {
590
+ writeOutMaybe(`Line style is ${topt.unicodeBorderLineStyle}.\n`);
591
+ return { status: 'ok' };
592
+ }
593
+ const lower = value.toLowerCase();
594
+ // Preserve 'old-ascii' verbatim so the bulk-view (`\pset` with no
595
+ // args) round-trips and the printer can pick the matching glyph
596
+ // table. Upstream `do_pset("linestyle", "old-ascii", …)` flips
597
+ // `popt.topt.line_style = &pg_asciiformat_old`; we carry the same
598
+ // three-way distinction on `unicodeBorderLineStyle`.
599
+ if (lower === 'ascii' || lower === 'old-ascii' || lower === 'unicode') {
600
+ const ls = lower;
601
+ topt.unicodeBorderLineStyle = ls;
602
+ topt.unicodeColumnLineStyle = ls;
603
+ topt.unicodeHeaderLineStyle = ls;
604
+ writeOutMaybe(`Line style is ${ls}.\n`);
605
+ return { status: 'ok' };
606
+ }
607
+ writeErr(`\\pset: allowed line styles are ascii, old-ascii, unicode\n`);
608
+ return { status: 'error', errorWritten: true };
609
+ }
610
+ case 'columns': {
611
+ if (value !== null) {
612
+ const n = parseInt(value, 10);
613
+ if (!Number.isFinite(n) || n < 0) {
614
+ writeErr(`\\pset: invalid columns "${value}"\n`);
615
+ return { status: 'error', errorWritten: true };
616
+ }
617
+ topt.columns = n;
618
+ }
619
+ // Upstream `printPsetInfo` reports `0` as the special "unset"
620
+ // sentinel — see `command.c:5433`.
621
+ if (topt.columns === 0) {
622
+ writeOutMaybe('Target width is unset.\n');
623
+ }
624
+ else {
625
+ writeOutMaybe(`Target width is ${topt.columns}.\n`);
626
+ }
627
+ return { status: 'ok' };
628
+ }
629
+ case 'xheader_width': {
630
+ if (value !== null) {
631
+ const lower = value.toLowerCase();
632
+ if (lower === 'full' || lower === 'column' || lower === 'page') {
633
+ topt.xheaderWidth = lower;
634
+ }
635
+ else {
636
+ const n = parseInt(value, 10);
637
+ if (!Number.isFinite(n) ||
638
+ n <= 0 ||
639
+ !/^[+]?\d+$/.test(value.trim())) {
640
+ writeErr(`\\pset: allowed xheader_width values are "full" (default), "column", "page", or a number specifying the exact width\n`);
641
+ return { status: 'error', errorWritten: true };
642
+ }
643
+ topt.xheaderWidth = n;
644
+ }
645
+ }
646
+ // Upstream `printPsetInfo` quotes the three named widths
647
+ // ("full" / "column" / "page") but renders the numeric form
648
+ // unquoted as `Expanded header width is 33.`.
649
+ const current = topt.xheaderWidth ?? 'full';
650
+ if (typeof current === 'number') {
651
+ writeOutMaybe(`Expanded header width is ${current}.\n`);
652
+ }
653
+ else {
654
+ writeOutMaybe(`Expanded header width is "${current}".\n`);
655
+ }
656
+ return { status: 'ok' };
657
+ }
658
+ case 'unicode_border_linestyle':
659
+ case 'unicode_column_linestyle':
660
+ case 'unicode_header_linestyle': {
661
+ // Upstream `printPsetInfo` renders these as
662
+ // `Unicode border line style is "single".` etc. — note the space
663
+ // between "line" and "style" in the message (the option name
664
+ // itself is one token, `linestyle`).
665
+ const which = opt === 'unicode_border_linestyle'
666
+ ? 'border'
667
+ : opt === 'unicode_column_linestyle'
668
+ ? 'column'
669
+ : 'header';
670
+ if (value !== null) {
671
+ const lower = value.toLowerCase();
672
+ if (lower !== 'single' && lower !== 'double') {
673
+ writeErr(`\\pset: ${opt} must be single or double\n`);
674
+ return { status: 'error', errorWritten: true };
675
+ }
676
+ const style = lower;
677
+ if (opt === 'unicode_border_linestyle') {
678
+ topt.unicodeBorderStyle = style;
679
+ }
680
+ else if (opt === 'unicode_column_linestyle') {
681
+ topt.unicodeColumnStyle = style;
682
+ }
683
+ else {
684
+ topt.unicodeHeaderStyle = style;
685
+ }
686
+ }
687
+ const current = opt === 'unicode_border_linestyle'
688
+ ? (topt.unicodeBorderStyle ?? 'single')
689
+ : opt === 'unicode_column_linestyle'
690
+ ? (topt.unicodeColumnStyle ?? 'single')
691
+ : (topt.unicodeHeaderStyle ?? 'single');
692
+ writeOutMaybe(`Unicode ${which} line style is "${current}".\n`);
693
+ return { status: 'ok' };
694
+ }
695
+ default: {
696
+ writeErr(`\\pset: unknown option "${option}"\n`);
697
+ return { status: 'error', errorWritten: true };
698
+ }
699
+ }
700
+ };
701
+ /**
702
+ * Wrap a string value in single quotes, escaping embedded newlines and
703
+ * single quotes. Mirrors upstream `pset_quoted_string` in
704
+ * `src/bin/psql/command.c` — used by the bulk-view formatter so the
705
+ * emitted line can be fed back into `\pset NAME VALUE`.
706
+ */
707
+ const psetQuotedString = (str) => {
708
+ let out = "'";
709
+ for (const ch of str) {
710
+ if (ch === '\n')
711
+ out += '\\n';
712
+ else if (ch === "'")
713
+ out += "\\'";
714
+ else
715
+ out += ch;
716
+ }
717
+ out += "'";
718
+ return out;
719
+ };
720
+ /**
721
+ * Render the numeric pager encoding upstream uses in `printPsetInfo`:
722
+ * 0 = never, 1 = "if needed" (our `'on'`), 2 = always. We keep
723
+ * `topt.pager` as the upstream-style triple ('off'|'on'|'always') for
724
+ * `applyPset`'s state machine; this is only the bulk-view conversion.
725
+ */
726
+ const pagerNumeric = (pager) => pager === 'off' ? 0 : pager === 'on' ? 1 : 2;
727
+ /**
728
+ * Render `xheader_width` for the bulk view. Enum values print verbatim;
729
+ * numeric values print as the integer.
730
+ */
731
+ const xheaderWidthDisplay = (w) => (typeof w === 'number' ? String(w) : w);
732
+ /**
733
+ * Print the full current `\pset` state, one option per line, to stdout.
734
+ * Used when `\pset` is invoked with no arguments. String-valued settings
735
+ * are single-quoted (matching upstream `pset_value_string`); `tableattr`
736
+ * and `title` are unquoted-empty when unset. The set, ordering, and
737
+ * column-spacing mirror `printPsetInfo` in `src/bin/psql/command.c`.
738
+ */
739
+ const printAllPset = (topt) => {
740
+ writeOut(`border ${topt.border}\n`);
741
+ writeOut(`columns ${topt.columns}\n`);
742
+ writeOut(`csv_fieldsep ${psetQuotedString(topt.csvFieldSep)}\n`);
743
+ writeOut(`expanded ${topt.expanded}\n`);
744
+ writeOut(`fieldsep ${psetQuotedString(topt.fieldSep)}\n`);
745
+ // fieldsep_zero / recordsep_zero are derived: upstream emits "on" iff
746
+ // the corresponding separator is the NUL byte.
747
+ writeOut(`fieldsep_zero ${topt.fieldSep === '\0' ? 'on' : 'off'}\n`);
748
+ writeOut(`footer ${topt.defaultFooter ? 'on' : 'off'}\n`);
749
+ writeOut(`format ${topt.format}\n`);
750
+ writeOut(`linestyle ${topt.unicodeBorderLineStyle}\n`);
751
+ writeOut(`null ${psetQuotedString(topt.nullPrint)}\n`);
752
+ writeOut(`numericlocale ${topt.numericLocale ? 'on' : 'off'}\n`);
753
+ // pager is emitted numerically (0/1/2) — upstream uses %d in printPsetInfo.
754
+ writeOut(`pager ${pagerNumeric(topt.pager)}\n`);
755
+ writeOut(`pager_min_lines ${topt.pagerMinLines}\n`);
756
+ writeOut(`recordsep ${psetQuotedString(topt.recordSep)}\n`);
757
+ writeOut(`recordsep_zero ${topt.recordSep === '\0' ? 'on' : 'off'}\n`);
758
+ writeOut(`tableattr ${topt.tableAttr === null ? '' : psetQuotedString(topt.tableAttr)}\n`);
759
+ writeOut(`title ${topt.title === null ? '' : psetQuotedString(topt.title)}\n`);
760
+ writeOut(`tuples_only ${topt.tuplesOnly ? 'on' : 'off'}\n`);
761
+ writeOut(`unicode_border_linestyle ${topt.unicodeBorderStyle ?? 'single'}\n`);
762
+ writeOut(`unicode_column_linestyle ${topt.unicodeColumnStyle ?? 'single'}\n`);
763
+ writeOut(`unicode_header_linestyle ${topt.unicodeHeaderStyle ?? 'single'}\n`);
764
+ writeOut(`xheader_width ${xheaderWidthDisplay(topt.xheaderWidth ?? 'full')}\n`);
765
+ };
766
+ const lexRawArgs = (tail) => {
767
+ const out = [];
768
+ let i = 0;
769
+ const isSpace = (c) => c === ' ' ||
770
+ c === '\t' ||
771
+ c === '\n' ||
772
+ c === '\r' ||
773
+ c === '\f' ||
774
+ c === '\v';
775
+ while (i < tail.length) {
776
+ while (i < tail.length && isSpace(tail[i]))
777
+ i++;
778
+ if (i >= tail.length)
779
+ break;
780
+ if (tail[i] === '\\')
781
+ break;
782
+ let arg = '';
783
+ while (i < tail.length && !isSpace(tail[i]) && tail[i] !== '\\') {
784
+ const c = tail[i];
785
+ if (c === "'") {
786
+ // Single-quoted: keep delimiters in the warning text so the user
787
+ // sees the literal token.
788
+ let j = i + 1;
789
+ let inner = '';
790
+ while (j < tail.length && tail[j] !== "'") {
791
+ if (tail[j] === '\\' && j + 1 < tail.length) {
792
+ inner += tail[j] + tail[j + 1];
793
+ j += 2;
794
+ continue;
795
+ }
796
+ inner += tail[j];
797
+ j++;
798
+ }
799
+ arg += `'${inner}'`;
800
+ i = j < tail.length ? j + 1 : j;
801
+ continue;
802
+ }
803
+ if (c === '"') {
804
+ let j = i + 1;
805
+ let inner = '';
806
+ while (j < tail.length && tail[j] !== '"') {
807
+ inner += tail[j];
808
+ j++;
809
+ }
810
+ arg += `"${inner}"`;
811
+ i = j < tail.length ? j + 1 : j;
812
+ continue;
813
+ }
814
+ if (c === '`') {
815
+ // Drop the backtick delimiters; don't run the command (OT_NO_EVAL).
816
+ let j = i + 1;
817
+ let inner = '';
818
+ while (j < tail.length && tail[j] !== '`') {
819
+ inner += tail[j];
820
+ j++;
821
+ }
822
+ arg += inner;
823
+ i = j < tail.length ? j + 1 : j;
824
+ continue;
825
+ }
826
+ arg += c;
827
+ i++;
828
+ }
829
+ out.push({ arg, endIdx: i });
830
+ }
831
+ return out;
832
+ };
833
+ /**
834
+ * `\pset [option [value]]` — the master print-options setter.
835
+ *
836
+ * - No args: print all options.
837
+ * - Option only: toggle (for booleans) or show current value.
838
+ * - Option + value: set.
839
+ * - Option + value + extra: set, then emit one `extra argument "X" ignored`
840
+ * per extra arg (matches upstream `exec_command_pset`'s post-`do_pset`
841
+ * call to `ignore_slash_options`).
842
+ *
843
+ * Implementation note: the surrounding REPL's `BackslashContext` (built
844
+ * in `core/mainloop.ts::makeBackslashContext`) returns the full
845
+ * `rawArgs` verbatim from `restOfLine()` — there's no cursor we can
846
+ * read to find the unconsumed tail. We therefore lex `rawArgs` a second
847
+ * time with {@link lexExtraArgs} to recover the *raw* (unexpanded)
848
+ * token text, while still calling `ctx.nextArg('normal')` for
849
+ * option/value so variable substitution and backtick execution behave
850
+ * exactly like the rest of the slash-command layer. The drain pulls
851
+ * the raw token text from index 2+ so the warning preserves the
852
+ * upstream `OT_NO_EVAL` semantics — `:foo` stays as `:foo`, `` `cmd` ``
853
+ * collapses to `cmd` without running.
854
+ */
855
+ export const cmdPset = {
856
+ name: 'pset',
857
+ helpKey: 'pset',
858
+ run: (ctx) => {
859
+ // Upstream `exec_command_pset` calls `psql_scan_slash_option` twice
860
+ // (with `OT_NORMAL`, `evaluate=true`) for option + value, then loops
861
+ // `psql_scan_slash_option(scan_state, OT_NORMAL, NULL, false)` with
862
+ // `evaluate=false` over the remainder to emit the "extra argument …
863
+ // ignored" warnings. Our `BackslashContext.nextArg('normal')` route
864
+ // pre-parses the entire `rawArgs` through `scanSlashArgs` on the
865
+ // first call, which evaluates EVERY backtick — so an invocation
866
+ // like `\pset fieldsep | \`nosuchcommand\` :foo` would spawn the
867
+ // shell for `nosuchcommand` even though upstream never runs it.
868
+ //
869
+ // To match upstream's no-eval semantics we split the lex in two:
870
+ // 1. Walk `rawArgs` no-eval with {@link lexRawArgs} to recover the
871
+ // byte boundary that ends arg #2 (option + value).
872
+ // 2. Re-parse the truncated prefix with `scanSlashArgs('normal')`
873
+ // so option/value get their proper `:var` / `` ` ` `` expansion.
874
+ // 3. Walk the tail with `lexRawArgs` no-eval and emit one
875
+ // "extra argument … ignored" warning per token, using the raw
876
+ // (unexpanded) text so `:foo` stays `:foo` and `` `cmd` ``
877
+ // collapses to `cmd` without running.
878
+ const rawEntries = lexRawArgs(ctx.rawArgs);
879
+ if (rawEntries.length === 0) {
880
+ printAllPset(ctx.settings.popt.topt);
881
+ return Promise.resolve({ status: 'ok' });
882
+ }
883
+ // Slice rawArgs up to the end of arg #2 (or arg #1 if only one was
884
+ // provided) and feed THAT to the eval-mode scanner. Anything past
885
+ // that boundary lives in the no-eval extras zone.
886
+ const headEndIdx = rawEntries.length >= 2 ? rawEntries[1].endIdx : rawEntries[0].endIdx;
887
+ const headSlice = ctx.rawArgs.slice(0, headEndIdx);
888
+ const varLookup = (name) => ctx.settings.vars.get(name);
889
+ const headArgs = scanSlashArgs(headSlice, 'normal', varLookup);
890
+ const option = headArgs[0] ?? null;
891
+ if (option === null) {
892
+ printAllPset(ctx.settings.popt.topt);
893
+ return Promise.resolve({ status: 'ok' });
894
+ }
895
+ const value = headArgs[1] ?? null;
896
+ // Under `--quiet` / `\set QUIET on`, upstream `exec_command_pset`
897
+ // (and the printPsetInfo helper it delegates to) suppresses the
898
+ // confirmation lines like `Null display is "…".` and `Tuples only
899
+ // is on.`. Pass `silent=true` so applyPset skips the writes —
900
+ // errors (invalid option / bad value) still go to stderr.
901
+ const result = applyPset(ctx.settings.popt.topt, option, value, ctx.cmdName, ctx.settings.quiet);
902
+ // Drain extras. Index 2+ in the no-eval lex are the tokens past
903
+ // option+value; emit one warning per raw token.
904
+ for (let i = 2; i < rawEntries.length; i++) {
905
+ writeErr(`\\${ctx.cmdName}: extra argument "${rawEntries[i].arg}" ignored\n`);
906
+ }
907
+ return Promise.resolve(result);
908
+ },
909
+ };