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.
- package/README.md +84 -0
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/connection_string.js +9 -1
- package/commands/functions.js +268 -0
- package/commands/index.js +4 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +6 -1
- package/functions_api.js +43 -0
- package/package.json +15 -5
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/esbuild.js +147 -0
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/checkout.test.js +0 -170
- package/commands/connection_string.test.js +0 -196
- package/commands/data_api.test.js +0 -169
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/link.test.js +0 -381
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/psql.test.js +0 -49
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/context.test.js +0 -119
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- 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
|
+
};
|