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,396 @@
|
|
|
1
|
+
import { formatNumericLocale } from './units.js';
|
|
2
|
+
/**
|
|
3
|
+
* LaTeX printers — `latex` (tabular) and `latex-longtable` (longtable).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors print.c `print_latex_text`, `print_latex_vertical`, and
|
|
6
|
+
* `print_latex_longtable_text`. Expanded mode for both `latex` and
|
|
7
|
+
* `latex-longtable` falls through to the vertical renderer
|
|
8
|
+
* (`print_latex_vertical`) — that's how upstream dispatches.
|
|
9
|
+
*
|
|
10
|
+
* Border behavior (flat tabular, from print.c):
|
|
11
|
+
* - `topt.border` is clamped to 0..3.
|
|
12
|
+
* - tabular column spec gets ` | ` between columns when border > 0,
|
|
13
|
+
* and a leading/trailing `|` at border >= 2.
|
|
14
|
+
* - border == 2 emits `\hline` around header and at end of table.
|
|
15
|
+
* - border == 3 emits `\hline` after every row.
|
|
16
|
+
*
|
|
17
|
+
* Border behavior (expanded, from print.c):
|
|
18
|
+
* - `topt.border` is clamped to 0..2 (note: not 0..3).
|
|
19
|
+
* - tabular column spec is `cl`, `c|l`, or `|c|l|` for border 0/1/2.
|
|
20
|
+
* - border >= 1 emits `\hline` between records.
|
|
21
|
+
* - border == 2 wraps each "Record N" header in `\hline` lines.
|
|
22
|
+
*
|
|
23
|
+
* Numeric columns (per the OID heuristic) get the `r` alignment letter;
|
|
24
|
+
* everything else gets `l`. That letter is passed straight through to
|
|
25
|
+
* LaTeX's column spec.
|
|
26
|
+
*
|
|
27
|
+
* Escape: 14 LaTeX-special characters are rewritten. Embedded newlines
|
|
28
|
+
* in a cell turn into `\\` (LaTeX line break) for `latex`. The
|
|
29
|
+
* longtable variant uses the same escape but wraps cells in
|
|
30
|
+
* `\raggedright{...}` and ends rows with `\tabularnewline`.
|
|
31
|
+
*/
|
|
32
|
+
// INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC, INTERVAL.
|
|
33
|
+
const NUMERIC_OIDS = new Set([21, 23, 20, 700, 701, 1700, 1186]);
|
|
34
|
+
export const latexPrinter = {
|
|
35
|
+
format: 'latex',
|
|
36
|
+
printQuery(rs, opts, out) {
|
|
37
|
+
const topt = opts.topt;
|
|
38
|
+
if (topt.expanded === 'on') {
|
|
39
|
+
return printLatexVertical(rs, opts, out);
|
|
40
|
+
}
|
|
41
|
+
return printLatexFlat(rs, opts, out);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const printLatexFlat = (rs, opts, out) => {
|
|
45
|
+
const topt = opts.topt;
|
|
46
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
47
|
+
const startTable = topt.startTable;
|
|
48
|
+
const stopTable = topt.stopTable;
|
|
49
|
+
const border = clampBorder(topt.border, 3);
|
|
50
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
51
|
+
const title = opts.title ?? topt.title;
|
|
52
|
+
const footers = opts.footers ?? topt.footers;
|
|
53
|
+
const headers = rs.fields.map((f) => f.name);
|
|
54
|
+
const ncols = rs.fields.length;
|
|
55
|
+
const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? 'r' : 'l');
|
|
56
|
+
const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
57
|
+
let buf = '';
|
|
58
|
+
if (startTable) {
|
|
59
|
+
if (!tuplesOnly && title) {
|
|
60
|
+
buf += '\\begin{center}\n';
|
|
61
|
+
buf += escapeLatex(title);
|
|
62
|
+
buf += '\n\\end{center}\n\n';
|
|
63
|
+
}
|
|
64
|
+
buf += '\\begin{tabular}{';
|
|
65
|
+
if (border >= 2)
|
|
66
|
+
buf += '| ';
|
|
67
|
+
aligns.forEach((a, idx) => {
|
|
68
|
+
buf += a;
|
|
69
|
+
if (border !== 0 && idx < ncols - 1)
|
|
70
|
+
buf += ' | ';
|
|
71
|
+
});
|
|
72
|
+
if (border >= 2)
|
|
73
|
+
buf += ' |';
|
|
74
|
+
buf += '}\n';
|
|
75
|
+
if (!tuplesOnly && border >= 2)
|
|
76
|
+
buf += '\\hline\n';
|
|
77
|
+
if (!tuplesOnly) {
|
|
78
|
+
headers.forEach((h, idx) => {
|
|
79
|
+
if (idx !== 0)
|
|
80
|
+
buf += ' & ';
|
|
81
|
+
buf += '\\textit{' + escapeLatex(h) + '}';
|
|
82
|
+
});
|
|
83
|
+
buf += ' \\\\\n';
|
|
84
|
+
buf += '\\hline\n';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
cells.forEach((row) => {
|
|
88
|
+
row.forEach((value, idx) => {
|
|
89
|
+
buf += escapeLatex(value);
|
|
90
|
+
if (idx === ncols - 1) {
|
|
91
|
+
buf += ' \\\\\n';
|
|
92
|
+
if (border === 3)
|
|
93
|
+
buf += '\\hline\n';
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
buf += ' & ';
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
if (stopTable) {
|
|
101
|
+
if (border === 2)
|
|
102
|
+
buf += '\\hline\n';
|
|
103
|
+
buf += '\\end{tabular}\n\n\\noindent ';
|
|
104
|
+
if (!tuplesOnly) {
|
|
105
|
+
const effective = effectiveFooters(rs, topt, footers);
|
|
106
|
+
for (const f of effective) {
|
|
107
|
+
buf += escapeLatex(f) + ' \\\\\n';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
buf += '\n';
|
|
111
|
+
}
|
|
112
|
+
out.write(buf);
|
|
113
|
+
return Promise.resolve();
|
|
114
|
+
};
|
|
115
|
+
const printLatexVertical = (rs, opts, out) => {
|
|
116
|
+
const topt = opts.topt;
|
|
117
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
118
|
+
const startTable = topt.startTable;
|
|
119
|
+
const stopTable = topt.stopTable;
|
|
120
|
+
const border = clampBorder(topt.border, 2);
|
|
121
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
122
|
+
const title = opts.title ?? topt.title;
|
|
123
|
+
const footers = opts.footers ?? topt.footers;
|
|
124
|
+
const headers = rs.fields.map((f) => f.name);
|
|
125
|
+
const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
126
|
+
let buf = '';
|
|
127
|
+
if (startTable) {
|
|
128
|
+
if (!tuplesOnly && title) {
|
|
129
|
+
buf += '\\begin{center}\n';
|
|
130
|
+
buf += escapeLatex(title);
|
|
131
|
+
buf += '\n\\end{center}\n\n';
|
|
132
|
+
}
|
|
133
|
+
buf += '\\begin{tabular}{';
|
|
134
|
+
if (border === 0)
|
|
135
|
+
buf += 'cl';
|
|
136
|
+
else if (border === 1)
|
|
137
|
+
buf += 'c|l';
|
|
138
|
+
else
|
|
139
|
+
buf += '|c|l|';
|
|
140
|
+
buf += '}\n';
|
|
141
|
+
}
|
|
142
|
+
let record = topt.prior + 1;
|
|
143
|
+
cells.forEach((row) => {
|
|
144
|
+
if (!tuplesOnly) {
|
|
145
|
+
if (border === 2) {
|
|
146
|
+
buf += '\\hline\n';
|
|
147
|
+
buf += `\\multicolumn{2}{|c|}{\\textit{Record ${String(record)}}} \\\\\n`;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
buf += `\\multicolumn{2}{c}{\\textit{Record ${String(record)}}} \\\\\n`;
|
|
151
|
+
}
|
|
152
|
+
record += 1;
|
|
153
|
+
}
|
|
154
|
+
if (border >= 1)
|
|
155
|
+
buf += '\\hline\n';
|
|
156
|
+
row.forEach((value, idx) => {
|
|
157
|
+
buf += escapeLatex(headers[idx]);
|
|
158
|
+
buf += ' & ';
|
|
159
|
+
buf += escapeLatex(value);
|
|
160
|
+
buf += ' \\\\\n';
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
if (stopTable) {
|
|
164
|
+
if (border === 2)
|
|
165
|
+
buf += '\\hline\n';
|
|
166
|
+
buf += '\\end{tabular}\n\n\\noindent ';
|
|
167
|
+
// Expanded mode does NOT include the default "(N rows)" footer;
|
|
168
|
+
// only user-supplied footers (matches print_latex_vertical).
|
|
169
|
+
if (!tuplesOnly && footers && footers.length > 0) {
|
|
170
|
+
for (const f of footers) {
|
|
171
|
+
buf += escapeLatex(f) + ' \\\\\n';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
buf += '\n';
|
|
175
|
+
}
|
|
176
|
+
out.write(buf);
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
};
|
|
179
|
+
export const latexLongtablePrinter = {
|
|
180
|
+
format: 'latex-longtable',
|
|
181
|
+
printQuery(rs, opts, out) {
|
|
182
|
+
// Upstream dispatch sends both `latex` and `latex-longtable` to
|
|
183
|
+
// `print_latex_vertical` when expanded is on (cf. print.c).
|
|
184
|
+
if (opts.topt.expanded === 'on') {
|
|
185
|
+
return printLatexVertical(rs, opts, out);
|
|
186
|
+
}
|
|
187
|
+
const topt = opts.topt;
|
|
188
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
189
|
+
const startTable = topt.startTable;
|
|
190
|
+
const stopTable = topt.stopTable;
|
|
191
|
+
const border = clampBorder(topt.border, 3);
|
|
192
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
193
|
+
const title = opts.title ?? topt.title;
|
|
194
|
+
const headers = rs.fields.map((f) => f.name);
|
|
195
|
+
const ncols = rs.fields.length;
|
|
196
|
+
const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? 'r' : 'l');
|
|
197
|
+
const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
198
|
+
// `topt.tableAttr` for longtable encodes per-column widths in a
|
|
199
|
+
// whitespace-separated list, consumed left-to-right with a fall
|
|
200
|
+
// back to the previous value once exhausted.
|
|
201
|
+
const widths = (topt.tableAttr ?? '')
|
|
202
|
+
.split(/[\s]+/)
|
|
203
|
+
.filter((w) => w !== '');
|
|
204
|
+
let widthCursor = 0;
|
|
205
|
+
let lastWidth = null;
|
|
206
|
+
let buf = '';
|
|
207
|
+
if (startTable) {
|
|
208
|
+
buf += '\\begin{longtable}{';
|
|
209
|
+
if (border >= 2)
|
|
210
|
+
buf += '| ';
|
|
211
|
+
aligns.forEach((a, idx) => {
|
|
212
|
+
if (a === 'l' && widths.length > 0) {
|
|
213
|
+
let w = null;
|
|
214
|
+
if (widthCursor < widths.length) {
|
|
215
|
+
w = widths[widthCursor];
|
|
216
|
+
widthCursor += 1;
|
|
217
|
+
lastWidth = w;
|
|
218
|
+
}
|
|
219
|
+
else if (lastWidth !== null) {
|
|
220
|
+
w = lastWidth;
|
|
221
|
+
}
|
|
222
|
+
if (w !== null) {
|
|
223
|
+
buf += `p{${w}\\textwidth}`;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
buf += 'l';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
buf += a;
|
|
231
|
+
}
|
|
232
|
+
if (border !== 0 && idx < ncols - 1)
|
|
233
|
+
buf += ' | ';
|
|
234
|
+
});
|
|
235
|
+
if (border >= 2)
|
|
236
|
+
buf += ' |';
|
|
237
|
+
buf += '}\n';
|
|
238
|
+
if (!tuplesOnly) {
|
|
239
|
+
// firsthead
|
|
240
|
+
if (border >= 2)
|
|
241
|
+
buf += '\\toprule\n';
|
|
242
|
+
headers.forEach((h, idx) => {
|
|
243
|
+
if (idx !== 0)
|
|
244
|
+
buf += ' & ';
|
|
245
|
+
buf += '\\small\\textbf{\\textit{' + escapeLatex(h) + '}}';
|
|
246
|
+
});
|
|
247
|
+
buf += ' \\\\\n';
|
|
248
|
+
buf += '\\midrule\n\\endfirsthead\n';
|
|
249
|
+
// continuation heads
|
|
250
|
+
if (border >= 2)
|
|
251
|
+
buf += '\\toprule\n';
|
|
252
|
+
headers.forEach((h, idx) => {
|
|
253
|
+
if (idx !== 0)
|
|
254
|
+
buf += ' & ';
|
|
255
|
+
buf += '\\small\\textbf{\\textit{' + escapeLatex(h) + '}}';
|
|
256
|
+
});
|
|
257
|
+
buf += ' \\\\\n';
|
|
258
|
+
if (border !== 3)
|
|
259
|
+
buf += '\\midrule\n';
|
|
260
|
+
buf += '\\endhead\n';
|
|
261
|
+
if (title) {
|
|
262
|
+
if (border === 2)
|
|
263
|
+
buf += '\\bottomrule\n';
|
|
264
|
+
buf +=
|
|
265
|
+
'\\caption[' +
|
|
266
|
+
escapeLatex(title) +
|
|
267
|
+
' (Continued)]{' +
|
|
268
|
+
escapeLatex(title) +
|
|
269
|
+
'}\n\\endfoot\n';
|
|
270
|
+
if (border === 2)
|
|
271
|
+
buf += '\\bottomrule\n';
|
|
272
|
+
buf +=
|
|
273
|
+
'\\caption[' +
|
|
274
|
+
escapeLatex(title) +
|
|
275
|
+
']{' +
|
|
276
|
+
escapeLatex(title) +
|
|
277
|
+
'}\n\\endlastfoot\n';
|
|
278
|
+
}
|
|
279
|
+
else if (border >= 2) {
|
|
280
|
+
buf += '\\bottomrule\n\\endfoot\n';
|
|
281
|
+
buf += '\\bottomrule\n\\endlastfoot\n';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Cells. Upstream interleaves `\n&\n` between in-row cells (and
|
|
286
|
+
// emits `\tabularnewline` to end a row), wrapping each value in
|
|
287
|
+
// `\raggedright{...}`.
|
|
288
|
+
let cellIdx = 0;
|
|
289
|
+
cells.forEach((row) => {
|
|
290
|
+
row.forEach((value) => {
|
|
291
|
+
if (cellIdx !== 0 && cellIdx % ncols !== 0)
|
|
292
|
+
buf += '\n&\n';
|
|
293
|
+
buf += '\\raggedright{' + escapeLatex(value) + '}';
|
|
294
|
+
if ((cellIdx + 1) % ncols === 0) {
|
|
295
|
+
buf += ' \\tabularnewline\n';
|
|
296
|
+
if (border === 3)
|
|
297
|
+
buf += ' \\hline\n';
|
|
298
|
+
}
|
|
299
|
+
cellIdx += 1;
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
if (stopTable)
|
|
303
|
+
buf += '\\end{longtable}\n';
|
|
304
|
+
out.write(buf);
|
|
305
|
+
return Promise.resolve();
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
const clampBorder = (b, max) => {
|
|
309
|
+
if (b > max)
|
|
310
|
+
return max;
|
|
311
|
+
if (b < 0)
|
|
312
|
+
return 0;
|
|
313
|
+
return b;
|
|
314
|
+
};
|
|
315
|
+
const effectiveFooters = (rs, topt, footers) => {
|
|
316
|
+
if (footers && footers.length > 0)
|
|
317
|
+
return footers;
|
|
318
|
+
if (topt.defaultFooter) {
|
|
319
|
+
const n = rs.rows.length;
|
|
320
|
+
return [`(${String(n)} ${n === 1 ? 'row' : 'rows'})`];
|
|
321
|
+
}
|
|
322
|
+
return [];
|
|
323
|
+
};
|
|
324
|
+
const escapeLatex = (input) => {
|
|
325
|
+
let out = '';
|
|
326
|
+
for (const ch of input) {
|
|
327
|
+
switch (ch) {
|
|
328
|
+
case '#':
|
|
329
|
+
out += '\\#';
|
|
330
|
+
break;
|
|
331
|
+
case '$':
|
|
332
|
+
out += '\\$';
|
|
333
|
+
break;
|
|
334
|
+
case '%':
|
|
335
|
+
out += '\\%';
|
|
336
|
+
break;
|
|
337
|
+
case '&':
|
|
338
|
+
out += '\\&';
|
|
339
|
+
break;
|
|
340
|
+
case '<':
|
|
341
|
+
out += '\\textless{}';
|
|
342
|
+
break;
|
|
343
|
+
case '>':
|
|
344
|
+
out += '\\textgreater{}';
|
|
345
|
+
break;
|
|
346
|
+
case '\\':
|
|
347
|
+
out += '\\textbackslash{}';
|
|
348
|
+
break;
|
|
349
|
+
case '^':
|
|
350
|
+
out += '\\^{}';
|
|
351
|
+
break;
|
|
352
|
+
case '_':
|
|
353
|
+
out += '\\_';
|
|
354
|
+
break;
|
|
355
|
+
case '{':
|
|
356
|
+
out += '\\{';
|
|
357
|
+
break;
|
|
358
|
+
case '|':
|
|
359
|
+
out += '\\textbar{}';
|
|
360
|
+
break;
|
|
361
|
+
case '}':
|
|
362
|
+
out += '\\}';
|
|
363
|
+
break;
|
|
364
|
+
case '~':
|
|
365
|
+
out += '\\~{}';
|
|
366
|
+
break;
|
|
367
|
+
case '\n':
|
|
368
|
+
out += '\\\\';
|
|
369
|
+
break;
|
|
370
|
+
default:
|
|
371
|
+
out += ch;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
};
|
|
376
|
+
const renderCell = (cell, nullPrint, numericLocale) => {
|
|
377
|
+
if (cell === null || cell === undefined)
|
|
378
|
+
return nullPrint;
|
|
379
|
+
if (typeof cell === 'string') {
|
|
380
|
+
return formatNumericLocale(cell, numericLocale);
|
|
381
|
+
}
|
|
382
|
+
if (typeof cell === 'number' || typeof cell === 'bigint') {
|
|
383
|
+
return formatNumericLocale(cell.toString(), numericLocale);
|
|
384
|
+
}
|
|
385
|
+
if (typeof cell === 'boolean')
|
|
386
|
+
return cell ? 't' : 'f';
|
|
387
|
+
if (cell instanceof Date)
|
|
388
|
+
return cell.toISOString();
|
|
389
|
+
if (cell instanceof Uint8Array) {
|
|
390
|
+
let hex = '\\x';
|
|
391
|
+
for (const b of cell)
|
|
392
|
+
hex += b.toString(16).padStart(2, '0');
|
|
393
|
+
return hex;
|
|
394
|
+
}
|
|
395
|
+
return JSON.stringify(cell);
|
|
396
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { accessSync, constants as fsConstants } from 'node:fs';
|
|
3
|
+
import { basename, delimiter, isAbsolute, join } from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Can `command` be spawned with `shell:false`? A bare name is searched on
|
|
6
|
+
* `$PATH`; a path is checked directly. Used to avoid `spawn(missingBinary,
|
|
7
|
+
* { shell:false })`, which does NOT throw synchronously (our try/catch never
|
|
8
|
+
* fires) but emits an async ENOENT — leaving writes to the dead child's stdin
|
|
9
|
+
* to vanish into a blank terminal (review item #19).
|
|
10
|
+
*/
|
|
11
|
+
const pagerCommandResolvable = (command) => {
|
|
12
|
+
const ok = (p) => {
|
|
13
|
+
try {
|
|
14
|
+
accessSync(p, fsConstants.X_OK);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// On Windows X_OK is not meaningful and a bare name omits `.exe`; fall
|
|
19
|
+
// back to a plain existence probe with common executable extensions.
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
for (const ext of ['', '.exe', '.cmd', '.bat']) {
|
|
22
|
+
try {
|
|
23
|
+
accessSync(p + ext, fsConstants.F_OK);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* keep trying */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
if (command.includes('/') || command.includes('\\') || isAbsolute(command)) {
|
|
35
|
+
return ok(command);
|
|
36
|
+
}
|
|
37
|
+
return (process.env.PATH ?? '')
|
|
38
|
+
.split(delimiter)
|
|
39
|
+
.filter((d) => d.length > 0)
|
|
40
|
+
.some((dir) => ok(join(dir, command)));
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the pager command string. Mirrors upstream:
|
|
44
|
+
* PSQL_PAGER → PAGER → DEFAULT_PAGER (`less` on POSIX, none on Windows).
|
|
45
|
+
*
|
|
46
|
+
* Empty / whitespace-only env values FALL THROUGH to the next candidate. This
|
|
47
|
+
* deliberately diverges from strict upstream (where `PSQL_PAGER=''` would
|
|
48
|
+
* disable the pager outright) because in Node a spawned child cannot easily
|
|
49
|
+
* "unset" an inherited env var — tests have to override it with the empty
|
|
50
|
+
* string. Treating empty values as "unset" matches the conformance spec
|
|
51
|
+
* (`tests/psql-conformance/tap/030_pager.spec.ts`) and lets `PSQL_PAGER=''`
|
|
52
|
+
* fall through to PAGER, which is the user-friendly interpretation.
|
|
53
|
+
*
|
|
54
|
+
* Users who want to disable the pager unconditionally should set
|
|
55
|
+
* `\pset pager off` (preferred) or unset both env vars before launch.
|
|
56
|
+
*/
|
|
57
|
+
const resolvePagerCmd = (opts) => {
|
|
58
|
+
const env = opts.env ?? process.env;
|
|
59
|
+
const candidates = [
|
|
60
|
+
opts.pagerCmd,
|
|
61
|
+
env.PSQL_PAGER,
|
|
62
|
+
env.PAGER,
|
|
63
|
+
];
|
|
64
|
+
for (const c of candidates) {
|
|
65
|
+
if (c === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
// Empty or whitespace-only → treat as "not set" and try the next slot.
|
|
68
|
+
if (/^\s*$/.test(c))
|
|
69
|
+
continue;
|
|
70
|
+
return c;
|
|
71
|
+
}
|
|
72
|
+
// DEFAULT_PAGER: `less` on POSIX; nothing on Windows.
|
|
73
|
+
if (process.platform === 'win32')
|
|
74
|
+
return '';
|
|
75
|
+
return 'less';
|
|
76
|
+
};
|
|
77
|
+
const getIsTty = (opts) => {
|
|
78
|
+
if (opts.isTty !== undefined)
|
|
79
|
+
return opts.isTty;
|
|
80
|
+
const stream = opts.stdout ?? process.stdout;
|
|
81
|
+
// Some test streams won't have isTTY.
|
|
82
|
+
const tty = stream.isTTY;
|
|
83
|
+
return Boolean(tty);
|
|
84
|
+
};
|
|
85
|
+
const getTerminalHeight = (opts) => {
|
|
86
|
+
if (opts.terminalHeight !== undefined)
|
|
87
|
+
return opts.terminalHeight;
|
|
88
|
+
const stream = opts.stdout ?? process.stdout;
|
|
89
|
+
const rows = stream.rows;
|
|
90
|
+
// If we can't tell, fall back to 24 (classic VT100 default).
|
|
91
|
+
return typeof rows === 'number' && rows > 0 ? rows : 24;
|
|
92
|
+
};
|
|
93
|
+
/** Standalone helper to determine whether a pager is needed at all. */
|
|
94
|
+
export const isPagerNeeded = (opts) => {
|
|
95
|
+
if (opts.pager === 'off')
|
|
96
|
+
return false;
|
|
97
|
+
const cmd = resolvePagerCmd(opts);
|
|
98
|
+
if (cmd === '')
|
|
99
|
+
return false;
|
|
100
|
+
if (opts.pager === 'always')
|
|
101
|
+
return true;
|
|
102
|
+
// pager === 'on'
|
|
103
|
+
if (!getIsTty(opts))
|
|
104
|
+
return false;
|
|
105
|
+
if (opts.lines === undefined)
|
|
106
|
+
return false;
|
|
107
|
+
const threshold = Math.max(opts.pagerMinLines, getTerminalHeight(opts));
|
|
108
|
+
return opts.lines >= threshold;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Decide whether a result of `rowCount` rows by `colCount` columns should be
|
|
112
|
+
* routed through the pager when written to `output`.
|
|
113
|
+
*
|
|
114
|
+
* NOTE on `pager === 'always'`: the TTY check is INTENTIONALLY skipped in
|
|
115
|
+
* this case. Upstream's `\pset pager always` is a user-explicit "force the
|
|
116
|
+
* pager on" override; honouring it on a pipe matches the integration test
|
|
117
|
+
* harness contract (see `tests/psql-conformance/tap/030_pager.spec.ts`) where
|
|
118
|
+
* the child process has no controlling TTY but the spec still requires the
|
|
119
|
+
* configured PAGER to be invoked. For `pager === 'on'` (auto mode) we keep
|
|
120
|
+
* the TTY guard so non-interactive runs don't spuriously spawn `less`.
|
|
121
|
+
*/
|
|
122
|
+
export const shouldPage = (opts) => {
|
|
123
|
+
if (opts.pager === 'off')
|
|
124
|
+
return false;
|
|
125
|
+
if (opts.redirectedOutput)
|
|
126
|
+
return false;
|
|
127
|
+
// Rough heuristic for "lines" — header (3) + rows + footer (1). Matches
|
|
128
|
+
// upstream `IsPagerNeeded` which counts rendered table lines.
|
|
129
|
+
const HEADER_LINES = 3;
|
|
130
|
+
const FOOTER_LINES = 1;
|
|
131
|
+
const estimatedLines = HEADER_LINES + Math.max(0, opts.rowCount) + FOOTER_LINES;
|
|
132
|
+
// TTY check: explicit override wins, else inspect the stream's own isTTY.
|
|
133
|
+
const isTty = opts.isTty !== undefined
|
|
134
|
+
? opts.isTty
|
|
135
|
+
: Boolean(opts.output.isTTY);
|
|
136
|
+
// `pager === 'always'` bypasses the TTY guard — see the docstring above.
|
|
137
|
+
// The remaining gates (resolved cmd, redirected output, off) are enforced
|
|
138
|
+
// inside `isPagerNeeded`.
|
|
139
|
+
if (opts.pager !== 'always' && !isTty)
|
|
140
|
+
return false;
|
|
141
|
+
return isPagerNeeded({
|
|
142
|
+
pager: opts.pager,
|
|
143
|
+
pagerMinLines: opts.pagerMinLines,
|
|
144
|
+
pagerCmd: opts.pagerCmd,
|
|
145
|
+
env: opts.env,
|
|
146
|
+
stdout: opts.output,
|
|
147
|
+
isTty,
|
|
148
|
+
terminalHeight: opts.terminalHeight,
|
|
149
|
+
lines: estimatedLines,
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
const SHELL_META = /[\s|;><]/;
|
|
153
|
+
const parsePagerCmd = (cmd) => {
|
|
154
|
+
// Match upstream behavior: when the value looks shell-y, hand it off to
|
|
155
|
+
// /bin/sh -c. Otherwise treat it as a direct argv[0].
|
|
156
|
+
if (SHELL_META.test(cmd)) {
|
|
157
|
+
return { command: cmd, args: [], shell: true };
|
|
158
|
+
}
|
|
159
|
+
return { command: cmd, args: [], shell: false };
|
|
160
|
+
};
|
|
161
|
+
const buildPagerEnv = (cmd, baseEnv) => {
|
|
162
|
+
const env = { ...baseEnv };
|
|
163
|
+
// psql sets LESS=FRX by default; mirror it when the resolved pager is
|
|
164
|
+
// `less` and the caller hasn't already set LESS.
|
|
165
|
+
if (env.LESS === undefined) {
|
|
166
|
+
// Pull out the first whitespace-separated token to detect `less` even
|
|
167
|
+
// when args follow (e.g. "less -S").
|
|
168
|
+
const firstToken = cmd.trim().split(/\s+/, 1)[0] ?? '';
|
|
169
|
+
const program = basename(firstToken);
|
|
170
|
+
if (program === 'less') {
|
|
171
|
+
env.LESS = 'FRX';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return env;
|
|
175
|
+
};
|
|
176
|
+
const noOpHandle = (out) => ({
|
|
177
|
+
out,
|
|
178
|
+
spawned: false,
|
|
179
|
+
close: () => Promise.resolve(0),
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* Returns a PagerHandle. Caller writes data to `out`, then calls `close()`.
|
|
183
|
+
* If no pager spawned (pager='off', not a TTY, or fewer lines than threshold),
|
|
184
|
+
* `out` is `stdout`.
|
|
185
|
+
*/
|
|
186
|
+
export const openPager = (opts) => {
|
|
187
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
188
|
+
if (!isPagerNeeded(opts)) {
|
|
189
|
+
return noOpHandle(stdout);
|
|
190
|
+
}
|
|
191
|
+
const cmd = resolvePagerCmd(opts);
|
|
192
|
+
// isPagerNeeded already verified cmd is non-empty, but guard for safety.
|
|
193
|
+
if (cmd === '') {
|
|
194
|
+
return noOpHandle(stdout);
|
|
195
|
+
}
|
|
196
|
+
const { command, shell } = parsePagerCmd(cmd);
|
|
197
|
+
// A missing pager binary with shell:false emits an async ENOENT that the
|
|
198
|
+
// try/catch below cannot catch, and the result would be silently-discarded
|
|
199
|
+
// output. Pre-check and fall back to stdout instead (review item #19).
|
|
200
|
+
// shell:true goes through `sh -c`, which always exists.
|
|
201
|
+
if (!shell && !pagerCommandResolvable(command)) {
|
|
202
|
+
return noOpHandle(stdout);
|
|
203
|
+
}
|
|
204
|
+
const baseEnv = opts.env ?? process.env;
|
|
205
|
+
const childEnv = buildPagerEnv(cmd, baseEnv);
|
|
206
|
+
let child;
|
|
207
|
+
try {
|
|
208
|
+
child = spawn(command, [], {
|
|
209
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
210
|
+
shell,
|
|
211
|
+
env: childEnv,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// If the pager fails to spawn, fall back to stdout (matches upstream:
|
|
216
|
+
// `if (pagerpipe) return pagerpipe; restore_sigpipe_trap(); ... return stdout`).
|
|
217
|
+
return noOpHandle(stdout);
|
|
218
|
+
}
|
|
219
|
+
const stdin = child.stdin;
|
|
220
|
+
if (stdin === null) {
|
|
221
|
+
// Should not happen given stdio: ['pipe', ...], but be defensive.
|
|
222
|
+
return noOpHandle(stdout);
|
|
223
|
+
}
|
|
224
|
+
// Swallow EPIPE: the user can quit the pager early, after which any
|
|
225
|
+
// pending writes will fail with EPIPE. Upstream relies on SIGPIPE being
|
|
226
|
+
// ignored to short-circuit the write loop; we just drop the error.
|
|
227
|
+
stdin.on('error', (err) => {
|
|
228
|
+
if (err.code !== 'EPIPE') {
|
|
229
|
+
// Re-throw anything unexpected.
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
const exitPromise = new Promise((resolve) => {
|
|
234
|
+
const settle = (code) => {
|
|
235
|
+
resolve(code ?? 0);
|
|
236
|
+
};
|
|
237
|
+
child.once('exit', (code) => {
|
|
238
|
+
settle(code);
|
|
239
|
+
});
|
|
240
|
+
child.once('error', () => {
|
|
241
|
+
// If the child errored (e.g. ENOENT), surface a non-zero exit code
|
|
242
|
+
// but don't throw — the caller's writes will have hit EPIPE which
|
|
243
|
+
// we already swallow.
|
|
244
|
+
settle(127);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
out: stdin,
|
|
249
|
+
spawned: true,
|
|
250
|
+
close: () => {
|
|
251
|
+
// End stdin then wait for the pager to drain & exit.
|
|
252
|
+
if (!stdin.writableEnded) {
|
|
253
|
+
try {
|
|
254
|
+
stdin.end();
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
const e = err;
|
|
258
|
+
if (e.code !== 'EPIPE')
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return exitPromise;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
};
|