neonctl 2.22.2 → 2.23.0
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 +277 -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 +44 -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,258 @@
|
|
|
1
|
+
import { formatNumericLocale } from './units.js';
|
|
2
|
+
/**
|
|
3
|
+
* Troff MS printer.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors print.c `print_troff_ms_text` and `print_troff_ms_vertical`.
|
|
6
|
+
*
|
|
7
|
+
* Output shape (flat, with title and one footer):
|
|
8
|
+
* .LP
|
|
9
|
+
* .DS C
|
|
10
|
+
* title
|
|
11
|
+
* .DE
|
|
12
|
+
* .LP
|
|
13
|
+
* .TS
|
|
14
|
+
* center;
|
|
15
|
+
* l | l | r.
|
|
16
|
+
* \fIcol1\fP \fIcol2\fP \fIcol3\fP
|
|
17
|
+
* _
|
|
18
|
+
* val1 val2 val3
|
|
19
|
+
* .TE
|
|
20
|
+
* .DS L
|
|
21
|
+
* (N rows)
|
|
22
|
+
* .DE
|
|
23
|
+
*
|
|
24
|
+
* Output shape (expanded, `\pset expanded on`):
|
|
25
|
+
* .LP
|
|
26
|
+
* .TS
|
|
27
|
+
* center;
|
|
28
|
+
* c s.
|
|
29
|
+
* \fIRecord 1\fP
|
|
30
|
+
* _ # if border>=1
|
|
31
|
+
* .T&
|
|
32
|
+
* c | l. # `c l.` when border != 1
|
|
33
|
+
* colname1 val1
|
|
34
|
+
* colname2 val2
|
|
35
|
+
* ...
|
|
36
|
+
* .T&
|
|
37
|
+
* c s.
|
|
38
|
+
* \fIRecord 2\fP
|
|
39
|
+
* ...
|
|
40
|
+
* .TE
|
|
41
|
+
* .DS L
|
|
42
|
+
* .DE
|
|
43
|
+
*
|
|
44
|
+
* - `topt.border` is clamped to 0..2. `border == 2` uses `center box;`;
|
|
45
|
+
* otherwise just `center;`. Border > 0 inserts ` | ` between column
|
|
46
|
+
* spec letters.
|
|
47
|
+
* - Numeric columns get `r`, others `l` (per the OID heuristic).
|
|
48
|
+
* - Tab is the field separator (consistent with `.TS` defaults).
|
|
49
|
+
* - The only structurally-hostile character is `\\`, which becomes
|
|
50
|
+
* `\(rs` (troff's "reverse solidus" glyph). Everything else passes
|
|
51
|
+
* through verbatim — troff ms is a byte stream.
|
|
52
|
+
* - Expanded mode emits a `.T&\n<spec>.\n` re-spec block between the
|
|
53
|
+
* "Record N" header (`c s.`) and the body (`c l.` / `c | l.`); under
|
|
54
|
+
* tuples-only the body spec is set once as `c l;` after the table
|
|
55
|
+
* header. No default `(N rows)` footer.
|
|
56
|
+
*/
|
|
57
|
+
// INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC, INTERVAL.
|
|
58
|
+
const NUMERIC_OIDS = new Set([21, 23, 20, 700, 701, 1700, 1186]);
|
|
59
|
+
export const troffMsPrinter = {
|
|
60
|
+
format: 'troff-ms',
|
|
61
|
+
printQuery(rs, opts, out) {
|
|
62
|
+
const topt = opts.topt;
|
|
63
|
+
if (topt.expanded === 'on') {
|
|
64
|
+
return printExpanded(rs, opts, out);
|
|
65
|
+
}
|
|
66
|
+
return printFlat(rs, opts, out);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const printFlat = (rs, opts, out) => {
|
|
70
|
+
const topt = opts.topt;
|
|
71
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
72
|
+
const startTable = topt.startTable;
|
|
73
|
+
const stopTable = topt.stopTable;
|
|
74
|
+
const border = clampBorder(topt.border);
|
|
75
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
76
|
+
const title = opts.title ?? topt.title;
|
|
77
|
+
const footers = opts.footers ?? topt.footers;
|
|
78
|
+
const headers = rs.fields.map((f) => f.name);
|
|
79
|
+
const ncols = rs.fields.length;
|
|
80
|
+
const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? 'r' : 'l');
|
|
81
|
+
const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
82
|
+
let buf = '';
|
|
83
|
+
if (startTable) {
|
|
84
|
+
if (!tuplesOnly && title) {
|
|
85
|
+
buf += '.LP\n.DS C\n';
|
|
86
|
+
buf += escapeTroff(title);
|
|
87
|
+
buf += '\n.DE\n';
|
|
88
|
+
}
|
|
89
|
+
buf += '.LP\n.TS\n';
|
|
90
|
+
buf += border === 2 ? 'center box;\n' : 'center;\n';
|
|
91
|
+
aligns.forEach((a, idx) => {
|
|
92
|
+
buf += a;
|
|
93
|
+
if (border > 0 && idx < ncols - 1)
|
|
94
|
+
buf += ' | ';
|
|
95
|
+
});
|
|
96
|
+
buf += '.\n';
|
|
97
|
+
if (!tuplesOnly) {
|
|
98
|
+
headers.forEach((h, idx) => {
|
|
99
|
+
if (idx !== 0)
|
|
100
|
+
buf += '\t';
|
|
101
|
+
buf += '\\fI' + escapeTroff(h) + '\\fP';
|
|
102
|
+
});
|
|
103
|
+
buf += '\n_\n';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
cells.forEach((row) => {
|
|
107
|
+
row.forEach((value, idx) => {
|
|
108
|
+
buf += escapeTroff(value);
|
|
109
|
+
if (idx === ncols - 1)
|
|
110
|
+
buf += '\n';
|
|
111
|
+
else
|
|
112
|
+
buf += '\t';
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
if (stopTable) {
|
|
116
|
+
buf += '.TE\n.DS L\n';
|
|
117
|
+
if (!tuplesOnly) {
|
|
118
|
+
const effective = effectiveFooters(rs, topt, footers);
|
|
119
|
+
for (const f of effective) {
|
|
120
|
+
buf += escapeTroff(f) + '\n';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
buf += '.DE\n';
|
|
124
|
+
}
|
|
125
|
+
out.write(buf);
|
|
126
|
+
return Promise.resolve();
|
|
127
|
+
};
|
|
128
|
+
const printExpanded = (rs, opts, out) => {
|
|
129
|
+
const topt = opts.topt;
|
|
130
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
131
|
+
const startTable = topt.startTable;
|
|
132
|
+
const stopTable = topt.stopTable;
|
|
133
|
+
const border = clampBorder(topt.border);
|
|
134
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
135
|
+
const title = opts.title ?? topt.title;
|
|
136
|
+
const footers = opts.footers ?? topt.footers;
|
|
137
|
+
const headers = rs.fields.map((f) => f.name);
|
|
138
|
+
const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
139
|
+
let buf = '';
|
|
140
|
+
// currentFormat: 0 = none yet, 1 = "Record N" header (c s),
|
|
141
|
+
// 2 = body (c l or c | l). Upstream uses the same tri-state to
|
|
142
|
+
// decide when to emit `.T&` separators.
|
|
143
|
+
let currentFormat = 0;
|
|
144
|
+
if (startTable) {
|
|
145
|
+
if (!tuplesOnly && title) {
|
|
146
|
+
buf += '.LP\n.DS C\n';
|
|
147
|
+
buf += escapeTroff(title);
|
|
148
|
+
buf += '\n.DE\n';
|
|
149
|
+
}
|
|
150
|
+
buf += '.LP\n.TS\n';
|
|
151
|
+
buf += border === 2 ? 'center box;\n' : 'center;\n';
|
|
152
|
+
// Under tuples-only, upstream emits a one-shot `c l;` body spec
|
|
153
|
+
// here so each Record's first .T& block is omitted.
|
|
154
|
+
if (tuplesOnly) {
|
|
155
|
+
buf += 'c l;\n';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Continuation: assume body spec is already in effect.
|
|
160
|
+
currentFormat = 2;
|
|
161
|
+
}
|
|
162
|
+
let record = topt.prior + 1;
|
|
163
|
+
cells.forEach((row, rowIdx) => {
|
|
164
|
+
if (!tuplesOnly) {
|
|
165
|
+
if (currentFormat !== 1) {
|
|
166
|
+
if (border === 2 && rowIdx > 0)
|
|
167
|
+
buf += '_\n';
|
|
168
|
+
if (currentFormat !== 0)
|
|
169
|
+
buf += '.T&\n';
|
|
170
|
+
buf += 'c s.\n';
|
|
171
|
+
currentFormat = 1;
|
|
172
|
+
}
|
|
173
|
+
buf += `\\fIRecord ${String(record)}\\fP\n`;
|
|
174
|
+
record += 1;
|
|
175
|
+
}
|
|
176
|
+
if (border >= 1)
|
|
177
|
+
buf += '_\n';
|
|
178
|
+
if (!tuplesOnly) {
|
|
179
|
+
if (currentFormat !== 2) {
|
|
180
|
+
if (currentFormat !== 0)
|
|
181
|
+
buf += '.T&\n';
|
|
182
|
+
buf += border !== 1 ? 'c l.\n' : 'c | l.\n';
|
|
183
|
+
currentFormat = 2;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
row.forEach((value, idx) => {
|
|
187
|
+
buf += escapeTroff(headers[idx]);
|
|
188
|
+
buf += '\t';
|
|
189
|
+
buf += escapeTroff(value);
|
|
190
|
+
buf += '\n';
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
if (stopTable) {
|
|
194
|
+
buf += '.TE\n.DS L\n';
|
|
195
|
+
// Expanded mode does NOT emit the default "(N rows)" footer.
|
|
196
|
+
if (!tuplesOnly && footers && footers.length > 0) {
|
|
197
|
+
for (const f of footers) {
|
|
198
|
+
buf += escapeTroff(f) + '\n';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
buf += '.DE\n';
|
|
202
|
+
}
|
|
203
|
+
out.write(buf);
|
|
204
|
+
return Promise.resolve();
|
|
205
|
+
};
|
|
206
|
+
const clampBorder = (b) => {
|
|
207
|
+
if (b > 2)
|
|
208
|
+
return 2;
|
|
209
|
+
if (b < 0)
|
|
210
|
+
return 0;
|
|
211
|
+
return b;
|
|
212
|
+
};
|
|
213
|
+
const effectiveFooters = (rs, topt, footers) => {
|
|
214
|
+
if (footers && footers.length > 0)
|
|
215
|
+
return footers;
|
|
216
|
+
if (topt.defaultFooter) {
|
|
217
|
+
const n = rs.rows.length;
|
|
218
|
+
return [`(${String(n)} ${n === 1 ? 'row' : 'rows'})`];
|
|
219
|
+
}
|
|
220
|
+
return [];
|
|
221
|
+
};
|
|
222
|
+
const escapeTroff = (input) => {
|
|
223
|
+
// Only `\` is dangerous (it begins a troff escape). Map it to `\(rs`,
|
|
224
|
+
// which renders the reverse-solidus glyph. Newlines flow through
|
|
225
|
+
// unchanged — `.TS` treats each line as a new row, so we never want
|
|
226
|
+
// raw newlines inside a cell; that constraint is enforced upstream
|
|
227
|
+
// by the caller, not here. (`renderCell` similarly never emits a raw
|
|
228
|
+
// newline today.)
|
|
229
|
+
let out = '';
|
|
230
|
+
for (const ch of input) {
|
|
231
|
+
if (ch === '\\')
|
|
232
|
+
out += '\\(rs';
|
|
233
|
+
else
|
|
234
|
+
out += ch;
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
};
|
|
238
|
+
const renderCell = (cell, nullPrint, numericLocale) => {
|
|
239
|
+
if (cell === null || cell === undefined)
|
|
240
|
+
return nullPrint;
|
|
241
|
+
if (typeof cell === 'string') {
|
|
242
|
+
return formatNumericLocale(cell, numericLocale);
|
|
243
|
+
}
|
|
244
|
+
if (typeof cell === 'number' || typeof cell === 'bigint') {
|
|
245
|
+
return formatNumericLocale(cell.toString(), numericLocale);
|
|
246
|
+
}
|
|
247
|
+
if (typeof cell === 'boolean')
|
|
248
|
+
return cell ? 't' : 'f';
|
|
249
|
+
if (cell instanceof Date)
|
|
250
|
+
return cell.toISOString();
|
|
251
|
+
if (cell instanceof Uint8Array) {
|
|
252
|
+
let hex = '\\x';
|
|
253
|
+
for (const b of cell)
|
|
254
|
+
hex += b.toString(16).padStart(2, '0');
|
|
255
|
+
return hex;
|
|
256
|
+
}
|
|
257
|
+
return JSON.stringify(cell);
|
|
258
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { formatNumericLocale } from './units.js';
|
|
2
|
+
/**
|
|
3
|
+
* Unaligned tabular printer.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors print.c `print_unaligned_text` / `print_unaligned_vertical`.
|
|
6
|
+
*
|
|
7
|
+
* Field separator defaults to `|`, record separator defaults to `\n`,
|
|
8
|
+
* both honor `opts.topt.fieldSep` / `recordSep`. NULL cells render
|
|
9
|
+
* `opts.nullPrint` (which falls back to `topt.nullPrint`, default `''`).
|
|
10
|
+
*
|
|
11
|
+
* Expanded (`\x`) mode prints `colname|value` per line with a blank
|
|
12
|
+
* record-sep gap between rows.
|
|
13
|
+
*
|
|
14
|
+
* The default footer `(N rows)` is only emitted when not in expanded
|
|
15
|
+
* mode and `topt.defaultFooter` is true; same as upstream psql.
|
|
16
|
+
*/
|
|
17
|
+
export const unalignedPrinter = {
|
|
18
|
+
format: 'unaligned',
|
|
19
|
+
printQuery(rs, opts, out) {
|
|
20
|
+
const topt = opts.topt;
|
|
21
|
+
const fieldSep = topt.fieldSep !== '' ? topt.fieldSep : '|';
|
|
22
|
+
const recordSep = topt.recordSep !== '' ? topt.recordSep : '\n';
|
|
23
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
24
|
+
const expanded = topt.expanded === 'on';
|
|
25
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
26
|
+
const headers = rs.fields.map((f) => f.name);
|
|
27
|
+
const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
28
|
+
let outBuf = '';
|
|
29
|
+
if (expanded) {
|
|
30
|
+
// Vertical mode mirrors print.c `print_unaligned_vertical`: each
|
|
31
|
+
// record gap is a DOUBLE recordSep, the title (if any) emits
|
|
32
|
+
// without a trailing separator (the loop handles that), inter-
|
|
33
|
+
// column lines get one recordSep, and the closing footer block is
|
|
34
|
+
// preceded by a recordSep with one more recordSep separating each
|
|
35
|
+
// footer entry.
|
|
36
|
+
let needRecordSep = false;
|
|
37
|
+
if (!tuplesOnly && opts.title) {
|
|
38
|
+
outBuf += opts.title;
|
|
39
|
+
needRecordSep = true;
|
|
40
|
+
}
|
|
41
|
+
cells.forEach((row) => {
|
|
42
|
+
if (needRecordSep) {
|
|
43
|
+
outBuf += recordSep + recordSep;
|
|
44
|
+
needRecordSep = false;
|
|
45
|
+
}
|
|
46
|
+
row.forEach((value, colIdx) => {
|
|
47
|
+
outBuf += headers[colIdx] + fieldSep + value;
|
|
48
|
+
if (colIdx < row.length - 1) {
|
|
49
|
+
outBuf += recordSep;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
needRecordSep = true;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
if (!tuplesOnly && opts.footers && opts.footers.length > 0) {
|
|
57
|
+
outBuf += recordSep;
|
|
58
|
+
for (const footer of opts.footers) {
|
|
59
|
+
outBuf += recordSep + footer;
|
|
60
|
+
}
|
|
61
|
+
needRecordSep = true;
|
|
62
|
+
}
|
|
63
|
+
if (needRecordSep)
|
|
64
|
+
outBuf += recordSep;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Horizontal mode.
|
|
68
|
+
if (!tuplesOnly && opts.title) {
|
|
69
|
+
outBuf += opts.title + recordSep;
|
|
70
|
+
}
|
|
71
|
+
if (!tuplesOnly) {
|
|
72
|
+
outBuf += headers.join(fieldSep) + recordSep;
|
|
73
|
+
}
|
|
74
|
+
cells.forEach((row) => {
|
|
75
|
+
outBuf += row.join(fieldSep) + recordSep;
|
|
76
|
+
});
|
|
77
|
+
// Default `(N rows)` footer is suppressed when the caller supplied its
|
|
78
|
+
// own footers — upstream (and aligned.ts) print user footers INSTEAD of
|
|
79
|
+
// the row count, not in addition (review: unaligned footer duplication).
|
|
80
|
+
const hasUserFooters = opts.footers !== undefined &&
|
|
81
|
+
opts.footers !== null &&
|
|
82
|
+
opts.footers.length > 0;
|
|
83
|
+
if (!tuplesOnly && topt.defaultFooter && !hasUserFooters) {
|
|
84
|
+
const n = rs.rows.length;
|
|
85
|
+
outBuf += `(${String(n)} ${n === 1 ? 'row' : 'rows'})` + recordSep;
|
|
86
|
+
}
|
|
87
|
+
if (!tuplesOnly && hasUserFooters) {
|
|
88
|
+
for (const footer of opts.footers ?? []) {
|
|
89
|
+
outBuf += footer + recordSep;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
out.write(outBuf);
|
|
94
|
+
return Promise.resolve();
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const renderCell = (cell, nullPrint, numericLocale) => {
|
|
98
|
+
if (cell === null || cell === undefined)
|
|
99
|
+
return nullPrint;
|
|
100
|
+
if (typeof cell === 'string') {
|
|
101
|
+
return formatNumericLocale(cell, numericLocale);
|
|
102
|
+
}
|
|
103
|
+
if (typeof cell === 'number' || typeof cell === 'bigint') {
|
|
104
|
+
return formatNumericLocale(cell.toString(), numericLocale);
|
|
105
|
+
}
|
|
106
|
+
if (typeof cell === 'boolean')
|
|
107
|
+
return cell ? 't' : 'f';
|
|
108
|
+
if (cell instanceof Date)
|
|
109
|
+
return cell.toISOString();
|
|
110
|
+
if (cell instanceof Uint8Array) {
|
|
111
|
+
// Bytea -> hex escape, matching libpq's `\x` form.
|
|
112
|
+
let hex = '\\x';
|
|
113
|
+
for (const b of cell)
|
|
114
|
+
hex += b.toString(16).padStart(2, '0');
|
|
115
|
+
return hex;
|
|
116
|
+
}
|
|
117
|
+
return JSON.stringify(cell);
|
|
118
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric, byte size, and duration formatting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors psql's `\pset numericlocale`, the server-side
|
|
5
|
+
* `pg_size_pretty()` output, and the `\timing` line format.
|
|
6
|
+
*/
|
|
7
|
+
const NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
8
|
+
/**
|
|
9
|
+
* Format a numeric string using locale-aware thousand separators and
|
|
10
|
+
* decimal point. Equivalent to print.c `format_numeric_locale` but
|
|
11
|
+
* implemented through the host's `Intl.NumberFormat`.
|
|
12
|
+
*
|
|
13
|
+
* When `useLocale` is false (the psql default) the input is returned
|
|
14
|
+
* unchanged. Only operates on values whose stringified form matches
|
|
15
|
+
* `^-?\d+(\.\d+)?$`; anything else (NaN, scientific notation, etc.) is
|
|
16
|
+
* returned as-is so we never mangle non-numeric data passed via
|
|
17
|
+
* `numericlocale`.
|
|
18
|
+
*/
|
|
19
|
+
export const formatNumericLocale = (value, useLocale, locale) => {
|
|
20
|
+
if (!useLocale)
|
|
21
|
+
return value;
|
|
22
|
+
if (!NUMERIC_RE.test(value))
|
|
23
|
+
return value;
|
|
24
|
+
const dotIdx = value.indexOf('.');
|
|
25
|
+
const fractionDigits = dotIdx === -1 ? 0 : value.length - dotIdx - 1;
|
|
26
|
+
// Use BigInt for the integer part to avoid float precision loss on
|
|
27
|
+
// very large integers; format the fractional component as a separate
|
|
28
|
+
// number so we keep the exact digit count the input had.
|
|
29
|
+
const negative = value.startsWith('-');
|
|
30
|
+
const unsigned = negative ? value.slice(1) : value;
|
|
31
|
+
const [intPart, fracPart = ''] = unsigned.split('.');
|
|
32
|
+
const intFormatted = new Intl.NumberFormat(locale, {
|
|
33
|
+
useGrouping: true,
|
|
34
|
+
maximumFractionDigits: 0,
|
|
35
|
+
}).format(BigInt(intPart));
|
|
36
|
+
if (fractionDigits === 0) {
|
|
37
|
+
return negative ? `-${intFormatted}` : intFormatted;
|
|
38
|
+
}
|
|
39
|
+
// Discover this locale's decimal separator.
|
|
40
|
+
const decimalSep = new Intl.NumberFormat(locale)
|
|
41
|
+
.formatToParts(1.1)
|
|
42
|
+
.find((p) => p.type === 'decimal')?.value ?? '.';
|
|
43
|
+
const out = `${intFormatted}${decimalSep}${fracPart}`;
|
|
44
|
+
return negative ? `-${out}` : out;
|
|
45
|
+
};
|
|
46
|
+
const BYTE_UNITS = [
|
|
47
|
+
{ name: 'bytes', bits: 0 },
|
|
48
|
+
{ name: 'kB', bits: 10 },
|
|
49
|
+
{ name: 'MB', bits: 20 },
|
|
50
|
+
{ name: 'GB', bits: 30 },
|
|
51
|
+
{ name: 'TB', bits: 40 },
|
|
52
|
+
{ name: 'PB', bits: 50 },
|
|
53
|
+
];
|
|
54
|
+
const SI_UNITS = [
|
|
55
|
+
{ name: 'B', bits: 0 },
|
|
56
|
+
{ name: 'kB', bits: 10 },
|
|
57
|
+
{ name: 'MB', bits: 20 },
|
|
58
|
+
{ name: 'GB', bits: 30 },
|
|
59
|
+
{ name: 'TB', bits: 40 },
|
|
60
|
+
{ name: 'PB', bits: 50 },
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Pretty-print a byte count.
|
|
64
|
+
*
|
|
65
|
+
* Follows pg's `pg_size_pretty` ladder: values under 10 240 use the
|
|
66
|
+
* `bytes` unit, otherwise we promote until the magnitude is below
|
|
67
|
+
* 20 * 1024 in the next unit and emit one decimal place.
|
|
68
|
+
*
|
|
69
|
+
* `opts.si` swaps the smallest unit name from `bytes` to `B`. (psql
|
|
70
|
+
* itself does not have an SI flag, but we expose one because callers
|
|
71
|
+
* outside the print path want a shorter `0 B` rendering.)
|
|
72
|
+
*/
|
|
73
|
+
export const formatByteSize = (bytes, opts) => {
|
|
74
|
+
const units = opts?.si ? SI_UNITS : BYTE_UNITS;
|
|
75
|
+
const sign = bytes < 0 ? '-' : '';
|
|
76
|
+
let abs = Math.abs(bytes);
|
|
77
|
+
// Bytes unit: render integer count.
|
|
78
|
+
if (abs < 10 * 1024) {
|
|
79
|
+
return `${sign}${abs.toString()} ${units[0].name}`;
|
|
80
|
+
}
|
|
81
|
+
// Promote until the value, scaled to the next unit, would be below
|
|
82
|
+
// 20 * 1024. Mirrors the original loop's threshold check.
|
|
83
|
+
let unitIndex = 1;
|
|
84
|
+
abs = abs / 1024;
|
|
85
|
+
while (unitIndex < units.length - 1 && abs >= 20 * 1024) {
|
|
86
|
+
abs = abs / 1024;
|
|
87
|
+
unitIndex++;
|
|
88
|
+
}
|
|
89
|
+
// One decimal place once we leave the `bytes` regime, matching the
|
|
90
|
+
// `0 B`, `1.0 kB`, `1.5 MB` shape the WP spec calls out.
|
|
91
|
+
return `${sign}${abs.toFixed(1)} ${units[unitIndex].name}`;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Render a duration in the upstream `\timing` style. psql scales the
|
|
95
|
+
* granularity to keep the line compact:
|
|
96
|
+
*
|
|
97
|
+
* - `< 1 ms` → `Time: 0.123 ms` (three decimal digits)
|
|
98
|
+
* - `< 1 s` → `Time: 123.456 ms`
|
|
99
|
+
* - `< 1 min` → `Time: 12.345 s`
|
|
100
|
+
* - `< 1 hour` → `Time: 12 m 34.567 s`
|
|
101
|
+
* - `>= 1 hour` → `Time: 1 h 23 m 45.678 s`
|
|
102
|
+
*
|
|
103
|
+
* Mirrors the `PrintTiming()` ladder in `src/bin/psql/common.c`. Negative
|
|
104
|
+
* or non-finite inputs fall through the ladder as `0`.
|
|
105
|
+
*/
|
|
106
|
+
export const formatDurationMs = (ms) => {
|
|
107
|
+
return `Time: ${formatDurationBody(ms)}`;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Format just the scaled-duration body (without the `Time: ` prefix) so
|
|
111
|
+
* callers like the `\watch` header can reuse the same ladder without
|
|
112
|
+
* dragging the prefix along.
|
|
113
|
+
*/
|
|
114
|
+
export const formatDurationBody = (ms) => {
|
|
115
|
+
const safe = Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
116
|
+
// Under one second: render in milliseconds with three decimal places.
|
|
117
|
+
if (safe < 1000) {
|
|
118
|
+
return `${safe.toFixed(3)} ms`;
|
|
119
|
+
}
|
|
120
|
+
const totalSeconds = safe / 1000;
|
|
121
|
+
// Under one minute: render in seconds with three decimal places.
|
|
122
|
+
if (totalSeconds < 60) {
|
|
123
|
+
return `${totalSeconds.toFixed(3)} s`;
|
|
124
|
+
}
|
|
125
|
+
// Under one hour: `M m SS.sss s`.
|
|
126
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
127
|
+
const remainingSeconds = totalSeconds - totalMinutes * 60;
|
|
128
|
+
if (totalMinutes < 60) {
|
|
129
|
+
return `${String(totalMinutes)} m ${remainingSeconds.toFixed(3)} s`;
|
|
130
|
+
}
|
|
131
|
+
// One hour or more: `H h MM m SS.sss s`.
|
|
132
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
133
|
+
const minutes = totalMinutes - hours * 60;
|
|
134
|
+
return `${String(hours)} h ${String(minutes)} m ${remainingSeconds.toFixed(3)} s`;
|
|
135
|
+
};
|