neonctl 2.22.0 → 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 +242 -16
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/checkout.js +249 -0
- package/commands/connection_string.js +15 -2
- package/commands/data_api.js +286 -0
- package/commands/functions.js +277 -0
- package/commands/index.js +12 -0
- package/commands/link.js +667 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +62 -0
- package/commands/set_context.js +7 -2
- package/context.js +86 -14
- package/functions_api.js +44 -0
- package/index.js +3 -0
- package/package.json +60 -51
- 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/enrichers.js +18 -1
- package/utils/esbuild.js +147 -0
- package/utils/middlewares.js +1 -1
- 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/connection_string.test.js +0 -196
- 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/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,1756 @@
|
|
|
1
|
+
import { formatNumericLocale } from './units.js';
|
|
2
|
+
/**
|
|
3
|
+
* Aligned tabular printer (psql's default output mode).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors print.c `print_aligned_text` / `print_aligned_vertical`.
|
|
6
|
+
*
|
|
7
|
+
* Supports horizontal (default), expanded/vertical (`\x on`), and
|
|
8
|
+
* wrapped (`\pset format wrapped`) layouts. Borders 0..3 (0 = none,
|
|
9
|
+
* 1 = light internal rules, 2 = full box, 3 = heavy with row rules).
|
|
10
|
+
* Unicode mode (`unicodeBorderLineStyle === 'unicode'`) swaps the
|
|
11
|
+
* ASCII rule glyphs for U+2500-range box-drawing characters.
|
|
12
|
+
*/
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Public helpers: display-width primitives reused by other formats.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// East-Asian wide / fullwidth ranges from Unicode Standard Annex #11.
|
|
17
|
+
// Trimmed to the ranges psql's `ucs_wcwidth` considers width 2.
|
|
18
|
+
const WIDE_RANGES = [
|
|
19
|
+
[0x1100, 0x115f],
|
|
20
|
+
[0x2329, 0x232a],
|
|
21
|
+
[0x2e80, 0x303e],
|
|
22
|
+
[0x3041, 0x33ff],
|
|
23
|
+
[0x3400, 0x4dbf],
|
|
24
|
+
[0x4e00, 0x9fff],
|
|
25
|
+
[0xa000, 0xa4cf],
|
|
26
|
+
[0xac00, 0xd7a3],
|
|
27
|
+
[0xf900, 0xfaff],
|
|
28
|
+
[0xfe10, 0xfe19],
|
|
29
|
+
[0xfe30, 0xfe6f],
|
|
30
|
+
[0xff00, 0xff60],
|
|
31
|
+
[0xffe0, 0xffe6],
|
|
32
|
+
[0x1f300, 0x1f64f],
|
|
33
|
+
[0x1f900, 0x1f9ff],
|
|
34
|
+
[0x20000, 0x2fffd],
|
|
35
|
+
[0x30000, 0x3fffd], // CJK Extension G
|
|
36
|
+
];
|
|
37
|
+
// Combining marks: width 0. From Unicode general categories Mn/Me/Cf
|
|
38
|
+
// and the zero-width controls upstream treats as width 0.
|
|
39
|
+
const ZERO_RANGES = [
|
|
40
|
+
[0x0300, 0x036f],
|
|
41
|
+
[0x0483, 0x0489],
|
|
42
|
+
[0x0591, 0x05bd],
|
|
43
|
+
[0x05bf, 0x05bf],
|
|
44
|
+
[0x05c1, 0x05c2],
|
|
45
|
+
[0x05c4, 0x05c5],
|
|
46
|
+
[0x05c7, 0x05c7],
|
|
47
|
+
[0x0610, 0x061a],
|
|
48
|
+
[0x064b, 0x065f],
|
|
49
|
+
[0x0670, 0x0670],
|
|
50
|
+
[0x06d6, 0x06dc],
|
|
51
|
+
[0x06df, 0x06e4],
|
|
52
|
+
[0x06e7, 0x06e8],
|
|
53
|
+
[0x06ea, 0x06ed],
|
|
54
|
+
[0x0711, 0x0711],
|
|
55
|
+
[0x0730, 0x074a],
|
|
56
|
+
[0x07a6, 0x07b0],
|
|
57
|
+
[0x07eb, 0x07f3],
|
|
58
|
+
[0x0816, 0x0819],
|
|
59
|
+
[0x081b, 0x0823],
|
|
60
|
+
[0x0825, 0x0827],
|
|
61
|
+
[0x0829, 0x082d],
|
|
62
|
+
[0x0859, 0x085b],
|
|
63
|
+
[0x08d3, 0x08e1],
|
|
64
|
+
[0x08e3, 0x0902],
|
|
65
|
+
[0x093a, 0x093a],
|
|
66
|
+
[0x093c, 0x093c],
|
|
67
|
+
[0x0941, 0x0948],
|
|
68
|
+
[0x094d, 0x094d],
|
|
69
|
+
[0x0951, 0x0957],
|
|
70
|
+
[0x0962, 0x0963],
|
|
71
|
+
[0x0981, 0x0981],
|
|
72
|
+
[0x09bc, 0x09bc],
|
|
73
|
+
[0x09c1, 0x09c4],
|
|
74
|
+
[0x09cd, 0x09cd],
|
|
75
|
+
[0x09e2, 0x09e3],
|
|
76
|
+
[0x09fe, 0x09fe],
|
|
77
|
+
[0x0a01, 0x0a02],
|
|
78
|
+
[0x0a3c, 0x0a3c],
|
|
79
|
+
[0x0a41, 0x0a42],
|
|
80
|
+
[0x0a47, 0x0a48],
|
|
81
|
+
[0x0a4b, 0x0a4d],
|
|
82
|
+
[0x0a51, 0x0a51],
|
|
83
|
+
[0x0a70, 0x0a71],
|
|
84
|
+
[0x0a75, 0x0a75],
|
|
85
|
+
[0x0a81, 0x0a82],
|
|
86
|
+
[0x0abc, 0x0abc],
|
|
87
|
+
[0x0ac1, 0x0ac5],
|
|
88
|
+
[0x0ac7, 0x0ac8],
|
|
89
|
+
[0x0acd, 0x0acd],
|
|
90
|
+
[0x0ae2, 0x0ae3],
|
|
91
|
+
[0x0afa, 0x0aff],
|
|
92
|
+
[0x0b01, 0x0b01],
|
|
93
|
+
[0x0b3c, 0x0b3c],
|
|
94
|
+
[0x0b3f, 0x0b3f],
|
|
95
|
+
[0x0b41, 0x0b44],
|
|
96
|
+
[0x0b4d, 0x0b4d],
|
|
97
|
+
[0x0b55, 0x0b56],
|
|
98
|
+
[0x0b62, 0x0b63],
|
|
99
|
+
[0x0b82, 0x0b82],
|
|
100
|
+
[0x0bc0, 0x0bc0],
|
|
101
|
+
[0x0bcd, 0x0bcd],
|
|
102
|
+
[0x0c00, 0x0c00],
|
|
103
|
+
[0x0c04, 0x0c04],
|
|
104
|
+
[0x0c3e, 0x0c40],
|
|
105
|
+
[0x0c46, 0x0c48],
|
|
106
|
+
[0x0c4a, 0x0c4d],
|
|
107
|
+
[0x0c55, 0x0c56],
|
|
108
|
+
[0x0c62, 0x0c63],
|
|
109
|
+
[0x0c81, 0x0c81],
|
|
110
|
+
[0x0cbc, 0x0cbc],
|
|
111
|
+
[0x0cbf, 0x0cbf],
|
|
112
|
+
[0x0cc6, 0x0cc6],
|
|
113
|
+
[0x0ccc, 0x0ccd],
|
|
114
|
+
[0x0ce2, 0x0ce3],
|
|
115
|
+
[0x0d00, 0x0d01],
|
|
116
|
+
[0x0d3b, 0x0d3c],
|
|
117
|
+
[0x0d41, 0x0d44],
|
|
118
|
+
[0x0d4d, 0x0d4d],
|
|
119
|
+
[0x0d62, 0x0d63],
|
|
120
|
+
[0x0dca, 0x0dca],
|
|
121
|
+
[0x0dd2, 0x0dd4],
|
|
122
|
+
[0x0dd6, 0x0dd6],
|
|
123
|
+
[0x0e31, 0x0e31],
|
|
124
|
+
[0x0e34, 0x0e3a],
|
|
125
|
+
[0x0e47, 0x0e4e],
|
|
126
|
+
[0x0eb1, 0x0eb1],
|
|
127
|
+
[0x0eb4, 0x0ebc],
|
|
128
|
+
[0x0ec8, 0x0ecd],
|
|
129
|
+
[0x0f18, 0x0f19],
|
|
130
|
+
[0x0f35, 0x0f35],
|
|
131
|
+
[0x0f37, 0x0f37],
|
|
132
|
+
[0x0f39, 0x0f39],
|
|
133
|
+
[0x0f71, 0x0f7e],
|
|
134
|
+
[0x0f80, 0x0f84],
|
|
135
|
+
[0x0f86, 0x0f87],
|
|
136
|
+
[0x0f8d, 0x0f97],
|
|
137
|
+
[0x0f99, 0x0fbc],
|
|
138
|
+
[0x0fc6, 0x0fc6],
|
|
139
|
+
[0x1037, 0x1037],
|
|
140
|
+
[0x1039, 0x103a],
|
|
141
|
+
[0x108d, 0x108d],
|
|
142
|
+
[0x135d, 0x135f],
|
|
143
|
+
[0x1712, 0x1714],
|
|
144
|
+
[0x1732, 0x1734],
|
|
145
|
+
[0x1752, 0x1753],
|
|
146
|
+
[0x1772, 0x1773],
|
|
147
|
+
[0x17b4, 0x17b5],
|
|
148
|
+
[0x17b7, 0x17bd],
|
|
149
|
+
[0x17c6, 0x17c6],
|
|
150
|
+
[0x17c9, 0x17d3],
|
|
151
|
+
[0x17dd, 0x17dd],
|
|
152
|
+
[0x180b, 0x180d],
|
|
153
|
+
[0x1885, 0x1886],
|
|
154
|
+
[0x18a9, 0x18a9],
|
|
155
|
+
[0x1920, 0x1922],
|
|
156
|
+
[0x1927, 0x1928],
|
|
157
|
+
[0x1932, 0x1932],
|
|
158
|
+
[0x1939, 0x193b],
|
|
159
|
+
[0x1a17, 0x1a18],
|
|
160
|
+
[0x1a1b, 0x1a1b],
|
|
161
|
+
[0x1a56, 0x1a56],
|
|
162
|
+
[0x1a58, 0x1a5e],
|
|
163
|
+
[0x1a60, 0x1a60],
|
|
164
|
+
[0x1a62, 0x1a62],
|
|
165
|
+
[0x1a65, 0x1a6c],
|
|
166
|
+
[0x1a73, 0x1a7c],
|
|
167
|
+
[0x1a7f, 0x1a7f],
|
|
168
|
+
[0x1ab0, 0x1abd],
|
|
169
|
+
[0x1b00, 0x1b03],
|
|
170
|
+
[0x1b34, 0x1b34],
|
|
171
|
+
[0x1b36, 0x1b3a],
|
|
172
|
+
[0x1b3c, 0x1b3c],
|
|
173
|
+
[0x1b42, 0x1b42],
|
|
174
|
+
[0x1b6b, 0x1b73],
|
|
175
|
+
[0x1b80, 0x1b81],
|
|
176
|
+
[0x1ba2, 0x1ba5],
|
|
177
|
+
[0x1ba8, 0x1ba9],
|
|
178
|
+
[0x1bab, 0x1bad],
|
|
179
|
+
[0x1be6, 0x1be6],
|
|
180
|
+
[0x1be8, 0x1be9],
|
|
181
|
+
[0x1bed, 0x1bed],
|
|
182
|
+
[0x1bef, 0x1bf1],
|
|
183
|
+
[0x1c2c, 0x1c33],
|
|
184
|
+
[0x1c36, 0x1c37],
|
|
185
|
+
[0x1cd0, 0x1cd2],
|
|
186
|
+
[0x1cd4, 0x1ce0],
|
|
187
|
+
[0x1ce2, 0x1ce8],
|
|
188
|
+
[0x1ced, 0x1ced],
|
|
189
|
+
[0x1cf4, 0x1cf4],
|
|
190
|
+
[0x1cf8, 0x1cf9],
|
|
191
|
+
[0x1dc0, 0x1df9],
|
|
192
|
+
[0x1dfb, 0x1dff],
|
|
193
|
+
[0x200b, 0x200f],
|
|
194
|
+
[0x202a, 0x202e],
|
|
195
|
+
[0x2060, 0x206f],
|
|
196
|
+
[0x20d0, 0x20f0],
|
|
197
|
+
[0x2cef, 0x2cf1],
|
|
198
|
+
[0x2d7f, 0x2d7f],
|
|
199
|
+
[0x2de0, 0x2dff],
|
|
200
|
+
[0x302a, 0x302d],
|
|
201
|
+
[0x3099, 0x309a],
|
|
202
|
+
[0xa66f, 0xa672],
|
|
203
|
+
[0xa674, 0xa67d],
|
|
204
|
+
[0xa69e, 0xa69f],
|
|
205
|
+
[0xa6f0, 0xa6f1],
|
|
206
|
+
[0xa802, 0xa802],
|
|
207
|
+
[0xa806, 0xa806],
|
|
208
|
+
[0xa80b, 0xa80b],
|
|
209
|
+
[0xa825, 0xa826],
|
|
210
|
+
[0xa8c4, 0xa8c5],
|
|
211
|
+
[0xa8e0, 0xa8f1],
|
|
212
|
+
[0xa926, 0xa92d],
|
|
213
|
+
[0xa947, 0xa951],
|
|
214
|
+
[0xa980, 0xa982],
|
|
215
|
+
[0xa9b3, 0xa9b3],
|
|
216
|
+
[0xa9b6, 0xa9b9],
|
|
217
|
+
[0xa9bc, 0xa9bd],
|
|
218
|
+
[0xa9e5, 0xa9e5],
|
|
219
|
+
[0xaa29, 0xaa2e],
|
|
220
|
+
[0xaa31, 0xaa32],
|
|
221
|
+
[0xaa35, 0xaa36],
|
|
222
|
+
[0xaa43, 0xaa43],
|
|
223
|
+
[0xaa4c, 0xaa4c],
|
|
224
|
+
[0xaa7c, 0xaa7c],
|
|
225
|
+
[0xaab0, 0xaab0],
|
|
226
|
+
[0xaab2, 0xaab4],
|
|
227
|
+
[0xaab7, 0xaab8],
|
|
228
|
+
[0xaabe, 0xaabf],
|
|
229
|
+
[0xaac1, 0xaac1],
|
|
230
|
+
[0xaaec, 0xaaed],
|
|
231
|
+
[0xaaf6, 0xaaf6],
|
|
232
|
+
[0xabe5, 0xabe5],
|
|
233
|
+
[0xabe8, 0xabe8],
|
|
234
|
+
[0xabed, 0xabed],
|
|
235
|
+
[0xfb1e, 0xfb1e],
|
|
236
|
+
[0xfe00, 0xfe0f],
|
|
237
|
+
[0xfe20, 0xfe2f],
|
|
238
|
+
[0xfeff, 0xfeff],
|
|
239
|
+
[0xfff9, 0xfffb],
|
|
240
|
+
[0x101fd, 0x101fd],
|
|
241
|
+
[0x102e0, 0x102e0],
|
|
242
|
+
[0x10376, 0x1037a],
|
|
243
|
+
[0x10a01, 0x10a03],
|
|
244
|
+
[0x10a05, 0x10a06],
|
|
245
|
+
[0x10a0c, 0x10a0f],
|
|
246
|
+
[0x10a38, 0x10a3a],
|
|
247
|
+
[0x10a3f, 0x10a3f],
|
|
248
|
+
[0x10ae5, 0x10ae6],
|
|
249
|
+
[0xe0100, 0xe01ef],
|
|
250
|
+
];
|
|
251
|
+
const inRange = (cp, ranges) => {
|
|
252
|
+
// Binary search; ranges are sorted by start.
|
|
253
|
+
let lo = 0;
|
|
254
|
+
let hi = ranges.length - 1;
|
|
255
|
+
while (lo <= hi) {
|
|
256
|
+
const mid = (lo + hi) >> 1;
|
|
257
|
+
const entry = ranges[mid];
|
|
258
|
+
if (cp < entry[0])
|
|
259
|
+
hi = mid - 1;
|
|
260
|
+
else if (cp > entry[1])
|
|
261
|
+
lo = mid + 1;
|
|
262
|
+
else
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Visible terminal width of one Unicode code point.
|
|
269
|
+
*
|
|
270
|
+
* - C0/DEL control codes -> 0 (we don't try to render them; psql's
|
|
271
|
+
* `pg_wcwidth` returns -1 here, but treating them as 0 matches the
|
|
272
|
+
* way the value already passes through the rendering pipeline).
|
|
273
|
+
* - Zero-width combining marks / format chars -> 0.
|
|
274
|
+
* - East-Asian Wide / Fullwidth -> 2.
|
|
275
|
+
* - Everything else -> 1.
|
|
276
|
+
*/
|
|
277
|
+
const codePointWidth = (cp) => {
|
|
278
|
+
if (cp === 0)
|
|
279
|
+
return 0;
|
|
280
|
+
if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0))
|
|
281
|
+
return 0;
|
|
282
|
+
if (inRange(cp, ZERO_RANGES))
|
|
283
|
+
return 0;
|
|
284
|
+
if (inRange(cp, WIDE_RANGES))
|
|
285
|
+
return 2;
|
|
286
|
+
return 1;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Compute the visible terminal width of a string. Iterates code points
|
|
290
|
+
* (not UTF-16 code units), so surrogate pairs count once. Newlines and
|
|
291
|
+
* tabs are passed through; callers should split by '\n' first when they
|
|
292
|
+
* need per-line width — matches `pg_wcssize`.
|
|
293
|
+
*/
|
|
294
|
+
export const displayWidth = (text) => {
|
|
295
|
+
let width = 0;
|
|
296
|
+
for (const ch of text) {
|
|
297
|
+
width += codePointWidth(ch.codePointAt(0) ?? 0);
|
|
298
|
+
}
|
|
299
|
+
return width;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Pad `text` so its visible width is exactly `width`. Truncates the
|
|
303
|
+
* pre-existing string if it already exceeds `width` (caller is
|
|
304
|
+
* responsible for fitting; we do not truncate by default).
|
|
305
|
+
*
|
|
306
|
+
* For `center`, an odd remainder biases right (matches psql, which
|
|
307
|
+
* uses `nbspace / 2` for the left pad and `(nbspace + 1) / 2` for the
|
|
308
|
+
* right).
|
|
309
|
+
*/
|
|
310
|
+
export const padToWidth = (text, width, alignment) => {
|
|
311
|
+
const w = displayWidth(text);
|
|
312
|
+
if (w >= width)
|
|
313
|
+
return text;
|
|
314
|
+
const pad = width - w;
|
|
315
|
+
if (alignment === 'left')
|
|
316
|
+
return text + ' '.repeat(pad);
|
|
317
|
+
if (alignment === 'right')
|
|
318
|
+
return ' '.repeat(pad) + text;
|
|
319
|
+
const leftPad = pad >> 1;
|
|
320
|
+
const rightPad = pad - leftPad;
|
|
321
|
+
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
|
|
322
|
+
};
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Type heuristics & cell formatting (shared with vertical mode).
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// PG type OIDs that should render right-aligned. From upstream
|
|
327
|
+
// `column_type_alignment` / `print_aligned_text`'s `align[]` build.
|
|
328
|
+
//
|
|
329
|
+
// Upstream libpq looks the OID up against the server-side `pg_type.typcategory`
|
|
330
|
+
// (or the legacy hard-coded numeric category) and right-aligns anything with
|
|
331
|
+
// category 'N' (numeric). That catches custom domains over numeric types and
|
|
332
|
+
// extension-provided numeric/identifier types (`pg_lsn`, `xid8`, …) without a
|
|
333
|
+
// hard-coded OID list.
|
|
334
|
+
//
|
|
335
|
+
// We don't have access to a live catalog at print time, so we maintain a
|
|
336
|
+
// curated set of well-known numeric/identifier OIDs that ship with stock PG.
|
|
337
|
+
// Custom domains over numeric types and contrib types not listed here will
|
|
338
|
+
// fall back to left-alignment — documented limitation.
|
|
339
|
+
// Exactly mirrors upstream psql's `column_type_alignment()` (common.c): the
|
|
340
|
+
// fixed set int2/int4/int8, float4/float8, numeric, oid, xid, cid, money.
|
|
341
|
+
// NOT interval / pg_lsn / xid8 — psql left-aligns those (review: minor
|
|
342
|
+
// divergences — we previously right-aligned interval & pg_lsn and omitted
|
|
343
|
+
// xid & cid).
|
|
344
|
+
const RIGHT_ALIGNED_OIDS = new Set([
|
|
345
|
+
20,
|
|
346
|
+
21,
|
|
347
|
+
23,
|
|
348
|
+
26,
|
|
349
|
+
28,
|
|
350
|
+
29,
|
|
351
|
+
700,
|
|
352
|
+
701,
|
|
353
|
+
790,
|
|
354
|
+
1700, // numeric
|
|
355
|
+
]);
|
|
356
|
+
const isRightAlignedField = (oid) => RIGHT_ALIGNED_OIDS.has(oid);
|
|
357
|
+
const renderCell = (cell, nullPrint, numericLocale) => {
|
|
358
|
+
if (cell === null || cell === undefined)
|
|
359
|
+
return nullPrint;
|
|
360
|
+
if (typeof cell === 'string') {
|
|
361
|
+
return formatNumericLocale(cell, numericLocale);
|
|
362
|
+
}
|
|
363
|
+
if (typeof cell === 'number' || typeof cell === 'bigint') {
|
|
364
|
+
return formatNumericLocale(cell.toString(), numericLocale);
|
|
365
|
+
}
|
|
366
|
+
if (typeof cell === 'boolean')
|
|
367
|
+
return cell ? 't' : 'f';
|
|
368
|
+
if (cell instanceof Date)
|
|
369
|
+
return cell.toISOString();
|
|
370
|
+
if (cell instanceof Uint8Array) {
|
|
371
|
+
let hex = '\\x';
|
|
372
|
+
for (const b of cell)
|
|
373
|
+
hex += b.toString(16).padStart(2, '0');
|
|
374
|
+
return hex;
|
|
375
|
+
}
|
|
376
|
+
return JSON.stringify(cell);
|
|
377
|
+
};
|
|
378
|
+
const ASCII_GLYPHS = {
|
|
379
|
+
hrule: '-',
|
|
380
|
+
vrule: '|',
|
|
381
|
+
topLeft: '+',
|
|
382
|
+
topMid: '+',
|
|
383
|
+
topRight: '+',
|
|
384
|
+
midLeft: '+',
|
|
385
|
+
midMid: '+',
|
|
386
|
+
midRight: '+',
|
|
387
|
+
botLeft: '+',
|
|
388
|
+
botMid: '+',
|
|
389
|
+
botRight: '+',
|
|
390
|
+
headerNlLeft: ' ',
|
|
391
|
+
headerNlRight: '+',
|
|
392
|
+
wrapLeft: '.',
|
|
393
|
+
wrapRight: '.',
|
|
394
|
+
nlLeft: ' ',
|
|
395
|
+
nlRight: '+',
|
|
396
|
+
wrapRightBorder: true,
|
|
397
|
+
midvruleNl: '|',
|
|
398
|
+
midvruleWrap: '|',
|
|
399
|
+
midvruleBlank: '|',
|
|
400
|
+
};
|
|
401
|
+
// `pg_asciiformat_old` — the legacy ASCII renderer kept around for
|
|
402
|
+
// `\pset linestyle old-ascii`. Two visible quirks vs the modern ASCII
|
|
403
|
+
// glyphs:
|
|
404
|
+
// - left-side `+` markers in headers (header_nl_left), no right-side
|
|
405
|
+
// marker on either headers or data, and `wrap_right_border=false`
|
|
406
|
+
// (no trailing marker at the table edge);
|
|
407
|
+
// - the column separator on continuation rows changes — `:` when the
|
|
408
|
+
// joining cell has more `\n`-split lines below, `;` when it has more
|
|
409
|
+
// wrap-split lines, ` ` when the cell is exhausted (`midvrule_blank`).
|
|
410
|
+
const OLD_ASCII_GLYPHS = {
|
|
411
|
+
hrule: '-',
|
|
412
|
+
vrule: '|',
|
|
413
|
+
topLeft: '+',
|
|
414
|
+
topMid: '+',
|
|
415
|
+
topRight: '+',
|
|
416
|
+
midLeft: '+',
|
|
417
|
+
midMid: '+',
|
|
418
|
+
midRight: '+',
|
|
419
|
+
botLeft: '+',
|
|
420
|
+
botMid: '+',
|
|
421
|
+
botRight: '+',
|
|
422
|
+
headerNlLeft: '+',
|
|
423
|
+
headerNlRight: ' ',
|
|
424
|
+
wrapLeft: ' ',
|
|
425
|
+
wrapRight: ' ',
|
|
426
|
+
nlLeft: ' ',
|
|
427
|
+
nlRight: ' ',
|
|
428
|
+
wrapRightBorder: false,
|
|
429
|
+
midvruleNl: ':',
|
|
430
|
+
midvruleWrap: ';',
|
|
431
|
+
midvruleBlank: ' ',
|
|
432
|
+
};
|
|
433
|
+
// Light box-drawing glyphs (the only Unicode variant we expose today —
|
|
434
|
+
// `unicode_border_linestyle=double` / `unicode_column_linestyle=double` /
|
|
435
|
+
// `unicode_header_linestyle=double` are parsed by `cmd_format.ts` but the
|
|
436
|
+
// shared `Unicode2LineStyle` slot in `PrintTableOpts` only carries
|
|
437
|
+
// `ascii | unicode`, so all three settings collapse onto these "single"
|
|
438
|
+
// glyphs. Adding the double variant requires extending the type to keep
|
|
439
|
+
// `single`/`double` distinct (or carrying a separate side-channel field) —
|
|
440
|
+
// out of scope for this pass.
|
|
441
|
+
//
|
|
442
|
+
// Codepoints (verified against upstream `unicode_style` in print.c):
|
|
443
|
+
// U+2500 ─ Box Drawings Light Horizontal (hrule)
|
|
444
|
+
// U+2502 │ Box Drawings Light Vertical (vrule)
|
|
445
|
+
// U+250C ┌ Box Drawings Light Down and Right (topLeft)
|
|
446
|
+
// U+252C ┬ Box Drawings Light Down and Horizontal (topMid)
|
|
447
|
+
// U+2510 ┐ Box Drawings Light Down and Left (topRight)
|
|
448
|
+
// U+251C ├ Box Drawings Light Vertical and Right (midLeft)
|
|
449
|
+
// U+253C ┼ Box Drawings Light Vertical and Horiz. (midMid)
|
|
450
|
+
// U+2524 ┤ Box Drawings Light Vertical and Left (midRight)
|
|
451
|
+
// U+2514 └ Box Drawings Light Up and Right (botLeft)
|
|
452
|
+
// U+2534 ┴ Box Drawings Light Up and Horizontal (botMid)
|
|
453
|
+
// U+2518 ┘ Box Drawings Light Up and Left (botRight)
|
|
454
|
+
const UNICODE_GLYPHS = {
|
|
455
|
+
hrule: '─',
|
|
456
|
+
vrule: '│',
|
|
457
|
+
topLeft: '┌',
|
|
458
|
+
topMid: '┬',
|
|
459
|
+
topRight: '┐',
|
|
460
|
+
midLeft: '├',
|
|
461
|
+
midMid: '┼',
|
|
462
|
+
midRight: '┤',
|
|
463
|
+
botLeft: '└',
|
|
464
|
+
botMid: '┴',
|
|
465
|
+
botRight: '┘',
|
|
466
|
+
// U+21B5 ↵ "Downwards Arrow with Corner Leftwards" — newline marker.
|
|
467
|
+
// U+2026 … "Horizontal Ellipsis" — in-cell wrap marker.
|
|
468
|
+
// Matches `unicode_style` in upstream `fe_utils/print.c`.
|
|
469
|
+
headerNlLeft: ' ',
|
|
470
|
+
headerNlRight: '↵',
|
|
471
|
+
wrapLeft: '…',
|
|
472
|
+
wrapRight: '…',
|
|
473
|
+
nlLeft: ' ',
|
|
474
|
+
nlRight: '↵',
|
|
475
|
+
wrapRightBorder: true,
|
|
476
|
+
midvruleNl: '│',
|
|
477
|
+
midvruleWrap: '│',
|
|
478
|
+
midvruleBlank: '│',
|
|
479
|
+
};
|
|
480
|
+
const glyphsFor = (style) => {
|
|
481
|
+
if (style === 'unicode')
|
|
482
|
+
return UNICODE_GLYPHS;
|
|
483
|
+
if (style === 'old-ascii')
|
|
484
|
+
return OLD_ASCII_GLYPHS;
|
|
485
|
+
return ASCII_GLYPHS;
|
|
486
|
+
};
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
// Column width computation.
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
/**
|
|
491
|
+
* Compute the per-column max-width array used by horizontal layout.
|
|
492
|
+
* Considers header width and every line of every cell value. Returns
|
|
493
|
+
* widths in display-character units.
|
|
494
|
+
*
|
|
495
|
+
* Cells are pre-rendered with `nullPrint` and `numericLocale` already
|
|
496
|
+
* applied so width measurements match what the printer will emit.
|
|
497
|
+
*/
|
|
498
|
+
export const computeColumnWidths = (rs, topt) => {
|
|
499
|
+
const nullPrint = topt.nullPrint;
|
|
500
|
+
const numericLocale = topt.numericLocale;
|
|
501
|
+
const widths = rs.fields.map((f) => displayWidth(f.name));
|
|
502
|
+
for (const row of rs.rows) {
|
|
503
|
+
for (let i = 0; i < rs.fields.length; i++) {
|
|
504
|
+
const cellText = renderCell(row[i], nullPrint, numericLocale);
|
|
505
|
+
for (const line of cellText.split('\n')) {
|
|
506
|
+
const w = displayWidth(line);
|
|
507
|
+
if (w > widths[i])
|
|
508
|
+
widths[i] = w;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return widths;
|
|
513
|
+
};
|
|
514
|
+
const formatCell = (text) => {
|
|
515
|
+
const lines = text.split('\n');
|
|
516
|
+
let width = 0;
|
|
517
|
+
for (const l of lines) {
|
|
518
|
+
const w = displayWidth(l);
|
|
519
|
+
if (w > width)
|
|
520
|
+
width = w;
|
|
521
|
+
}
|
|
522
|
+
return { lines, width };
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Build a horizontal rule line (top, middle, bottom). Layout matches
|
|
526
|
+
* psql:
|
|
527
|
+
*
|
|
528
|
+
* border 0: `aa bb` -> `-- --` (no corners, single-space gaps)
|
|
529
|
+
* border 1: `-aa-+-bb-` (no outer corners, '+' between columns)
|
|
530
|
+
* border 2: `+-aa-+-bb-+` (full box)
|
|
531
|
+
* border 3: same as 2 here; row rules between data rows added elsewhere.
|
|
532
|
+
*
|
|
533
|
+
* The hrule covers `width + 2` characters per column when border >= 1
|
|
534
|
+
* (the content padding spaces become hrule chars on rule lines).
|
|
535
|
+
*/
|
|
536
|
+
const buildRule = (widths, border, glyphs, position) => {
|
|
537
|
+
const { hrule } = glyphs;
|
|
538
|
+
let left;
|
|
539
|
+
let mid;
|
|
540
|
+
let right;
|
|
541
|
+
if (position === 'top') {
|
|
542
|
+
left = glyphs.topLeft;
|
|
543
|
+
mid = glyphs.topMid;
|
|
544
|
+
right = glyphs.topRight;
|
|
545
|
+
}
|
|
546
|
+
else if (position === 'middle') {
|
|
547
|
+
left = glyphs.midLeft;
|
|
548
|
+
mid = glyphs.midMid;
|
|
549
|
+
right = glyphs.midRight;
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
left = glyphs.botLeft;
|
|
553
|
+
mid = glyphs.botMid;
|
|
554
|
+
right = glyphs.botRight;
|
|
555
|
+
}
|
|
556
|
+
let out = '';
|
|
557
|
+
if (border === 2 || border === 3) {
|
|
558
|
+
out += left;
|
|
559
|
+
}
|
|
560
|
+
for (let i = 0; i < widths.length; i++) {
|
|
561
|
+
// Per column the rule covers content width plus the two side pads
|
|
562
|
+
// that the data lines have for border >= 1.
|
|
563
|
+
const pad = border === 0 ? 0 : 1;
|
|
564
|
+
out += hrule.repeat(widths[i] + pad * 2);
|
|
565
|
+
if (i < widths.length - 1) {
|
|
566
|
+
if (border === 0) {
|
|
567
|
+
out += ' '; // single space between columns in border 0
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
out += mid;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (border === 2 || border === 3) {
|
|
575
|
+
out += right;
|
|
576
|
+
}
|
|
577
|
+
return out;
|
|
578
|
+
};
|
|
579
|
+
const renderHorizontal = (rs, opts, wrapped) => {
|
|
580
|
+
const topt = opts.topt;
|
|
581
|
+
const border = topt.border;
|
|
582
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
583
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
584
|
+
const glyphs = glyphsFor(topt.unicodeBorderLineStyle);
|
|
585
|
+
const headers = rs.fields.map((f) => f.name);
|
|
586
|
+
const aligns = rs.fields.map((f) => isRightAlignedField(f.dataTypeID) ? 'right' : 'left');
|
|
587
|
+
// Pre-render & measure every cell.
|
|
588
|
+
const cellGrid = rs.rows.map((row) => row.map((cell) => formatCell(renderCell(cell, nullPrint, topt.numericLocale))));
|
|
589
|
+
const headerCells = headers.map((h) => formatCell(h));
|
|
590
|
+
const colCount = rs.fields.length;
|
|
591
|
+
const widths = headerCells.map((c) => c.width);
|
|
592
|
+
for (const row of cellGrid) {
|
|
593
|
+
for (let i = 0; i < colCount; i++) {
|
|
594
|
+
if (row[i].width > widths[i])
|
|
595
|
+
widths[i] = row[i].width;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Wrapped mode shrinks the worst column until total width fits
|
|
599
|
+
// `topt.columns`. Mirrors upstream `print_aligned_text` in
|
|
600
|
+
// `fe_utils/print.c`: we score each column by
|
|
601
|
+
//
|
|
602
|
+
// ratio = current_width / average_width + max_width * 0.01
|
|
603
|
+
//
|
|
604
|
+
// and shrink the column with the highest ratio. That picks the column
|
|
605
|
+
// whose individual rows are mostly narrower than the worst-case row
|
|
606
|
+
// (so wrapping costs fewer extra lines than shrinking a uniformly-wide
|
|
607
|
+
// column would). The +max*0.01 bias breaks ties in favour of the
|
|
608
|
+
// overall-widest column, matching the upstream comment "Slightly bias
|
|
609
|
+
// against wider columns. (Increases chance a narrow column will fit
|
|
610
|
+
// in its cell.)".
|
|
611
|
+
//
|
|
612
|
+
// `width_wrap` (== `widths` here) starts as `max_width`. Each loop
|
|
613
|
+
// iteration decrements it by one until the total fits or no column
|
|
614
|
+
// can shrink further (every column is already at its header width).
|
|
615
|
+
if (wrapped) {
|
|
616
|
+
const maxColumns = topt.columns > 0 ? topt.columns : topt.envColumns;
|
|
617
|
+
if (maxColumns > 0) {
|
|
618
|
+
// Average width per column over the data rows (header excluded,
|
|
619
|
+
// matching upstream `width_average` which divides cell_count by
|
|
620
|
+
// col_count). Each cell's width is its max line width (mirrors
|
|
621
|
+
// upstream `pg_wcssize`, which returns the widest line of the
|
|
622
|
+
// possibly-multiline cell).
|
|
623
|
+
const widthAverage = new Array(colCount).fill(0);
|
|
624
|
+
const maxWidth = headerCells.map((c) => c.width);
|
|
625
|
+
if (cellGrid.length > 0) {
|
|
626
|
+
for (const row of cellGrid) {
|
|
627
|
+
for (let i = 0; i < colCount; i++) {
|
|
628
|
+
widthAverage[i] += row[i].width;
|
|
629
|
+
if (row[i].width > maxWidth[i])
|
|
630
|
+
maxWidth[i] = row[i].width;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
for (let i = 0; i < colCount; i++) {
|
|
634
|
+
widthAverage[i] /= cellGrid.length;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Total fixed overhead. Mirrors upstream `print.c` lines 763-769:
|
|
638
|
+
// border 0: col_count (one trailing-marker slot per cell)
|
|
639
|
+
// border 1: col_count * 3 - 1 (3 slots per cell minus shared sep)
|
|
640
|
+
// border 2: col_count * 3 + 1 (3 slots per cell plus outer rules)
|
|
641
|
+
// For ASCII / Unicode the trailing marker slot is always emitted
|
|
642
|
+
// (wrap_right_border=true), so the formula matches verbatim.
|
|
643
|
+
const overheadFor = (n) => {
|
|
644
|
+
if (border === 0)
|
|
645
|
+
return n;
|
|
646
|
+
if (border === 1)
|
|
647
|
+
return n * 3 - (n > 0 ? 1 : 0);
|
|
648
|
+
return n * 3 + 1;
|
|
649
|
+
};
|
|
650
|
+
const overhead = overheadFor(colCount);
|
|
651
|
+
const totalHeaderWidth = overhead + headerCells.reduce((a, c) => a + c.width, 0);
|
|
652
|
+
let total = overhead + widths.reduce((a, b) => a + b, 0);
|
|
653
|
+
// Upstream guards against shrinking when even the headers don't
|
|
654
|
+
// fit — there's no point picking columns to wrap if the rule row
|
|
655
|
+
// is already too wide.
|
|
656
|
+
if (maxColumns >= totalHeaderWidth) {
|
|
657
|
+
while (total > maxColumns) {
|
|
658
|
+
let worstCol = -1;
|
|
659
|
+
let maxRatio = 0;
|
|
660
|
+
for (let i = 0; i < colCount; i++) {
|
|
661
|
+
const headerW = headerCells[i].width;
|
|
662
|
+
// Two preconditions from upstream:
|
|
663
|
+
// - width_average[i] != 0 (column has some data; avoids /0)
|
|
664
|
+
// - width_wrap[i] > width_header[i] (header is the floor;
|
|
665
|
+
// can't shrink below it without overlapping the header).
|
|
666
|
+
if (widthAverage[i] > 0 && widths[i] > headerW) {
|
|
667
|
+
const ratio = widths[i] / widthAverage[i] + maxWidth[i] * 0.01;
|
|
668
|
+
if (ratio > maxRatio) {
|
|
669
|
+
maxRatio = ratio;
|
|
670
|
+
worstCol = i;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (worstCol === -1)
|
|
675
|
+
break;
|
|
676
|
+
widths[worstCol]--;
|
|
677
|
+
total--;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
let out = '';
|
|
683
|
+
const newline = '\n';
|
|
684
|
+
// Compute total table width once: needed for title centring (and only
|
|
685
|
+
// there at the moment). Mirrors upstream `width_total` from
|
|
686
|
+
// print.c lines 935-950: when the title is narrower than the table,
|
|
687
|
+
// centre it; otherwise left-align (no padding).
|
|
688
|
+
let widthTotal = 0;
|
|
689
|
+
if (!tuplesOnly && topt.title) {
|
|
690
|
+
const overhead = border === 0
|
|
691
|
+
? widths.length
|
|
692
|
+
: border === 1
|
|
693
|
+
? widths.length * 3 - (widths.length > 0 ? 1 : 0)
|
|
694
|
+
: widths.length * 3 + 1;
|
|
695
|
+
widthTotal = overhead + widths.reduce((a, b) => a + b, 0);
|
|
696
|
+
const titleW = displayWidth(topt.title);
|
|
697
|
+
if (titleW >= widthTotal) {
|
|
698
|
+
out += topt.title + newline;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
const pad = (widthTotal - titleW) >> 1;
|
|
702
|
+
out += ' '.repeat(pad) + topt.title + newline;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Top rule: emitted when border == 2 or 3.
|
|
706
|
+
if (!tuplesOnly && (border === 2 || border === 3)) {
|
|
707
|
+
out += buildRule(widths, border, glyphs, 'top') + newline;
|
|
708
|
+
}
|
|
709
|
+
// ------- Header row(s) -------
|
|
710
|
+
if (!tuplesOnly) {
|
|
711
|
+
// For wrapped mode the header column may itself need wrapping if its
|
|
712
|
+
// own width exceeds the (possibly shrunk) column width. Upstream
|
|
713
|
+
// `print_aligned_text` only computes header line breaks based on `\n`
|
|
714
|
+
// splits — header text that's wider than the column wraps onto the
|
|
715
|
+
// next display line via the same wrap loop as the data path. We
|
|
716
|
+
// mirror that by passing the header text through `wrapLine` in
|
|
717
|
+
// wrapped mode (and through the simple `\n` split for plain mode).
|
|
718
|
+
const headerColLines = headerCells.map((c, i) => {
|
|
719
|
+
if (!wrapped)
|
|
720
|
+
return c.lines;
|
|
721
|
+
const out = [];
|
|
722
|
+
for (const line of c.lines) {
|
|
723
|
+
out.push(...wrapLine(line, widths[i]));
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
});
|
|
727
|
+
const headerLineCount = Math.max(1, ...headerColLines.map((l) => l.length));
|
|
728
|
+
for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
|
|
729
|
+
// Per-column wrap state for this physical header line:
|
|
730
|
+
// `wrap` the cell has more content to emit on the next line
|
|
731
|
+
// via in-cell wrapping (wrap_left/wrap_right marker)
|
|
732
|
+
// `nl` the cell has more `\n`-split content below
|
|
733
|
+
// (header_nl_left/header_nl_right marker)
|
|
734
|
+
// `none` the cell is done (already emitted its last line)
|
|
735
|
+
const cellWrapPrev = headerColLines.map((l) => (lineIdx > 0 && lineIdx < l.length ? 'nl' : 'none'));
|
|
736
|
+
const cellWrapNext = headerColLines.map((l) => (lineIdx < l.length - 1 ? 'nl' : 'none'));
|
|
737
|
+
out += renderHeaderLine(headerColLines.map((l) => l[lineIdx] ?? ''), widths, border, glyphs, cellWrapPrev, cellWrapNext, lineIdx === 0);
|
|
738
|
+
out += newline;
|
|
739
|
+
}
|
|
740
|
+
// Header rule.
|
|
741
|
+
out += buildRule(widths, border, glyphs, 'middle') + newline;
|
|
742
|
+
}
|
|
743
|
+
// ------- Data rows -------
|
|
744
|
+
for (let r = 0; r < cellGrid.length; r++) {
|
|
745
|
+
const row = cellGrid[r];
|
|
746
|
+
// For wrapped mode, each cell may need to be re-broken to fit
|
|
747
|
+
// the (possibly shrunk) column width.
|
|
748
|
+
const colLines = row.map((cell, i) => {
|
|
749
|
+
const lines = cell.lines;
|
|
750
|
+
if (!wrapped)
|
|
751
|
+
return lines;
|
|
752
|
+
// Each source line may itself need breaking down to fit.
|
|
753
|
+
const out = [];
|
|
754
|
+
for (const line of lines) {
|
|
755
|
+
out.push(...wrapLine(line, widths[i]));
|
|
756
|
+
}
|
|
757
|
+
return out;
|
|
758
|
+
});
|
|
759
|
+
// We also need to know, per column, which output lines originated as
|
|
760
|
+
// wrap continuations (vs. `\n` splits) so the markers can pick the
|
|
761
|
+
// correct glyph: `.` (wrap_left/right) when wrapped from an
|
|
762
|
+
// over-width source line, `+` (nl_left/right) when split by a literal
|
|
763
|
+
// newline in the cell content.
|
|
764
|
+
//
|
|
765
|
+
// We classify by walking the original cell.lines: each source line
|
|
766
|
+
// expands to N display lines (N >= 1) via `wrapLine`. The first
|
|
767
|
+
// expansion is "nl" (or the very first line of the cell, which has
|
|
768
|
+
// no marker), the rest are "wrap" continuations of the same source
|
|
769
|
+
// line.
|
|
770
|
+
const colLineKinds = row.map((cell, i) => {
|
|
771
|
+
const kinds = [];
|
|
772
|
+
const lines = cell.lines;
|
|
773
|
+
for (let si = 0; si < lines.length; si++) {
|
|
774
|
+
const chunks = wrapped ? wrapLine(lines[si], widths[i]) : [lines[si]];
|
|
775
|
+
for (let ci = 0; ci < chunks.length; ci++) {
|
|
776
|
+
if (si === 0 && ci === 0)
|
|
777
|
+
kinds.push('start');
|
|
778
|
+
else if (ci === 0)
|
|
779
|
+
kinds.push('nl');
|
|
780
|
+
else
|
|
781
|
+
kinds.push('wrap');
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return kinds;
|
|
785
|
+
});
|
|
786
|
+
const lineCount = Math.max(1, ...colLines.map((l) => l.length));
|
|
787
|
+
for (let li = 0; li < lineCount; li++) {
|
|
788
|
+
// For each column, determine the wrap state for THIS output line.
|
|
789
|
+
// The LEFT marker is selected by `cellWrapPrev[j]`: what kind of
|
|
790
|
+
// continuation made this line exist (the `kind` of the current
|
|
791
|
+
// line — `wrap` or `nl` — only matters when li > 0).
|
|
792
|
+
// The RIGHT marker is selected by `cellWrapNext[j]`: the kind of
|
|
793
|
+
// the NEXT line (li + 1), which tells us whether this cell is
|
|
794
|
+
// still emitting more content.
|
|
795
|
+
const cellWrapPrev = colLineKinds.map((kinds, j) => {
|
|
796
|
+
if (li === 0 || li >= colLines[j].length)
|
|
797
|
+
return 'none';
|
|
798
|
+
return kinds[li] === 'wrap' ? 'wrap' : 'nl';
|
|
799
|
+
});
|
|
800
|
+
const cellWrapNext = colLineKinds.map((kinds, j) => {
|
|
801
|
+
const nextIdx = li + 1;
|
|
802
|
+
if (nextIdx >= colLines[j].length)
|
|
803
|
+
return 'none';
|
|
804
|
+
return kinds[nextIdx] === 'wrap' ? 'wrap' : 'nl';
|
|
805
|
+
});
|
|
806
|
+
out += renderDataLine(colLines.map((l) => l[li] ?? ''), widths, aligns, border, glyphs, cellWrapPrev, cellWrapNext, li === 0);
|
|
807
|
+
out += newline;
|
|
808
|
+
}
|
|
809
|
+
// Heavy borders (border == 3) insert a rule between rows.
|
|
810
|
+
if (border === 3 && r < cellGrid.length - 1) {
|
|
811
|
+
out += buildRule(widths, border, glyphs, 'middle') + newline;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Bottom rule when border 2/3.
|
|
815
|
+
if (!tuplesOnly && (border === 2 || border === 3)) {
|
|
816
|
+
out += buildRule(widths, border, glyphs, 'bottom') + newline;
|
|
817
|
+
}
|
|
818
|
+
// Footer: (N rows) + trailing blank line. Upstream's
|
|
819
|
+
// `print_aligned_text` emits `printTableAddFooter()` and then
|
|
820
|
+
// `printTableCleanup()` adds a separator `\n`. Verified against
|
|
821
|
+
// vanilla psql 18: `SELECT 1; SELECT 2;` emits each result followed
|
|
822
|
+
// by `(1 row)\n\n` so consecutive queries are visually separated.
|
|
823
|
+
//
|
|
824
|
+
// Footer handling mirrors `footers_with_default` (print.c lines
|
|
825
|
+
// 397-413): user footers SUPPRESS the default `(N rows)` row counter.
|
|
826
|
+
// The default is only emitted when there are no user footers AND
|
|
827
|
+
// `defaultFooter` is true. This matters for `\d` output, which
|
|
828
|
+
// attaches user footers ("Access method: ...", "Owned by: ...") and
|
|
829
|
+
// doesn't want the row counter to interleave between the data rows
|
|
830
|
+
// and the footers.
|
|
831
|
+
const hasUserFooters = !!opts.footers && opts.footers.length > 0;
|
|
832
|
+
if (!tuplesOnly && topt.defaultFooter && !hasUserFooters) {
|
|
833
|
+
const n = rs.rows.length;
|
|
834
|
+
out += `(${String(n)} ${n === 1 ? 'row' : 'rows'})` + newline;
|
|
835
|
+
}
|
|
836
|
+
if (!tuplesOnly && opts.footers) {
|
|
837
|
+
for (const f of opts.footers)
|
|
838
|
+
out += f + newline;
|
|
839
|
+
}
|
|
840
|
+
// Trailing blank line between query results. Upstream `print.c`
|
|
841
|
+
// line 1196 emits `fputc('\n', fout)` ONLY when `stop_table` is set —
|
|
842
|
+
// chunked-cursor mid-error printing leaves it unset so the ERROR line
|
|
843
|
+
// lands flush against the last data row. Default is `true`, so happy-path
|
|
844
|
+
// queries still get the blank separator before the next command.
|
|
845
|
+
if (topt.stopTable) {
|
|
846
|
+
out += newline;
|
|
847
|
+
}
|
|
848
|
+
return out;
|
|
849
|
+
};
|
|
850
|
+
/**
|
|
851
|
+
* Pick the column-separator glyph between two adjacent cells. ASCII and
|
|
852
|
+
* Unicode collapse `midvruleNl` / `midvruleWrap` / `midvruleBlank` onto the
|
|
853
|
+
* base `vrule`, so the alt-glyph branches only matter for `old-ascii`,
|
|
854
|
+
* which uses `:` for `\n` continuations, `;` for wrap continuations, and
|
|
855
|
+
* `" "` when the joining column is exhausted but the line still exists
|
|
856
|
+
* because of a wrap/continuation elsewhere in the row.
|
|
857
|
+
*
|
|
858
|
+
* Upstream picks the midvrule from the column *to the right of the
|
|
859
|
+
* separator* (`right`) — if that column is past-end, the separator
|
|
860
|
+
* collapses to `midvruleBlank` regardless of the left column's state.
|
|
861
|
+
*
|
|
862
|
+
* `firstLine = true` short-circuits to the base `vrule` — upstream
|
|
863
|
+
* only consults the midvrule table once at least one continuation has
|
|
864
|
+
* been emitted, so the first display line of every row always uses
|
|
865
|
+
* the regular vrule even if a column will wrap on the next line.
|
|
866
|
+
*/
|
|
867
|
+
const pickMidvrule = (glyphs,
|
|
868
|
+
// Left col's state at this line (cellWrapPrev[i]) — unused by the
|
|
869
|
+
// upstream rule but kept in the signature for clarity at call sites.
|
|
870
|
+
_left, right, firstLine) => {
|
|
871
|
+
if (firstLine)
|
|
872
|
+
return glyphs.vrule;
|
|
873
|
+
if (right === 'wrap')
|
|
874
|
+
return glyphs.midvruleWrap;
|
|
875
|
+
if (right === 'nl')
|
|
876
|
+
return glyphs.midvruleNl;
|
|
877
|
+
return glyphs.midvruleBlank;
|
|
878
|
+
};
|
|
879
|
+
/**
|
|
880
|
+
* Right-marker glyph for a data row's cell. Mirrors the `wrap[j]`-driven
|
|
881
|
+
* fputs at upstream `print.c` lines 1151-1156:
|
|
882
|
+
* PRINT_LINE_WRAP_WRAP → format->wrap_right (`.` ascii, `…` unicode)
|
|
883
|
+
* PRINT_LINE_WRAP_NEWLINE → format->nl_right (`+` ascii, `↵` unicode)
|
|
884
|
+
* PRINT_LINE_WRAP_NONE → " " (only when not at the trailing edge
|
|
885
|
+
* without border)
|
|
886
|
+
*/
|
|
887
|
+
const dataRightMarker = (state, glyphs, isLast, border) => {
|
|
888
|
+
if (state === 'wrap')
|
|
889
|
+
return glyphs.wrapRight;
|
|
890
|
+
if (state === 'nl')
|
|
891
|
+
return glyphs.nlRight;
|
|
892
|
+
// state === 'none': trailing edge — only emit a space when there's
|
|
893
|
+
// something AFTER us (another column to follow, or the right border
|
|
894
|
+
// of a border=2/3 box). For the LAST cell on a borderless row, nothing.
|
|
895
|
+
if (isLast && border !== 2 && border !== 3)
|
|
896
|
+
return '';
|
|
897
|
+
return ' ';
|
|
898
|
+
};
|
|
899
|
+
/**
|
|
900
|
+
* Left-marker glyph for a data row's cell. Mirrors `wrap[j]` at upstream
|
|
901
|
+
* `print.c` lines 1066-1073:
|
|
902
|
+
* PRINT_LINE_WRAP_WRAP → format->wrap_left (`.` ascii, `…` unicode)
|
|
903
|
+
* PRINT_LINE_WRAP_NEWLINE → format->nl_left (" " for both ascii/unicode)
|
|
904
|
+
* PRINT_LINE_WRAP_NONE → " "
|
|
905
|
+
*
|
|
906
|
+
* Only emitted when border != 0 (the leading-gutter slot). For border=0
|
|
907
|
+
* upstream skips this entirely (no leading marker).
|
|
908
|
+
*/
|
|
909
|
+
const dataLeftMarker = (state, glyphs) => {
|
|
910
|
+
if (state === 'wrap')
|
|
911
|
+
return glyphs.wrapLeft;
|
|
912
|
+
if (state === 'nl')
|
|
913
|
+
return glyphs.nlLeft;
|
|
914
|
+
return ' ';
|
|
915
|
+
};
|
|
916
|
+
/**
|
|
917
|
+
* Render one display-line of a header row. Centered cells, fixed
|
|
918
|
+
* `header_nl_*` markers on continuation lines.
|
|
919
|
+
*
|
|
920
|
+
* Layout (with ASCII glyphs):
|
|
921
|
+
* border 0: `<c1centered>[H]<c2centered>[H]` — `wrap_right_border=true`
|
|
922
|
+
* means the trailing slot still gets emitted even at table
|
|
923
|
+
* edge. `H` = `header_nl_right` ("+") when the next line in
|
|
924
|
+
* this column has more content, else " ".
|
|
925
|
+
* border 1: ` <c1centered>[H]| <c2centered>[H]`
|
|
926
|
+
* (leading gutter, header_nl_right marker between content and
|
|
927
|
+
* separator, optional trailing " " on the very last cell)
|
|
928
|
+
* border 2: `| <c1centered>[H]| <c2centered>[H]|` (full box)
|
|
929
|
+
*/
|
|
930
|
+
const renderHeaderLine = (cells, widths, border, glyphs,
|
|
931
|
+
// What kind of continuation produced THIS line (curr_nl_line > 0 in
|
|
932
|
+
// upstream): determines the leading marker on this side of the cell.
|
|
933
|
+
// Per-col `cellWrapPrev[i]` only drives the trailing marker now; the
|
|
934
|
+
// leading-gutter decision uses `firstLine` (curr_nl_line == 0).
|
|
935
|
+
cellWrapPrev,
|
|
936
|
+
// What kind of continuation follows on the NEXT line: determines
|
|
937
|
+
// the trailing marker for this cell.
|
|
938
|
+
cellWrapNext,
|
|
939
|
+
// True on the very first display line of the header (curr_nl_line == 0
|
|
940
|
+
// in upstream). Upstream emits `header_nl_left` for ALL non-first cells
|
|
941
|
+
// whenever curr_nl_line > 0 — even for cells that are themselves
|
|
942
|
+
// exhausted on that line — so the choice can't be made per-column.
|
|
943
|
+
firstLine) => {
|
|
944
|
+
const { vrule } = glyphs;
|
|
945
|
+
let out = '';
|
|
946
|
+
if (border === 2 || border === 3)
|
|
947
|
+
out += vrule;
|
|
948
|
+
for (let i = 0; i < cells.length; i++) {
|
|
949
|
+
const isLast = i === cells.length - 1;
|
|
950
|
+
// Leading gutter / wrap marker. Upstream `print.c` line 981-984:
|
|
951
|
+
// if (opt_border != 0 || (!format->wrap_right_border && i > 0))
|
|
952
|
+
// fputs(curr_nl_line ? format->header_nl_left : " ", fout);
|
|
953
|
+
// For ASCII (wrap_right_border=true): emitted iff border != 0.
|
|
954
|
+
// For old-ascii (wrap_right_border=false): emitted for non-first
|
|
955
|
+
// cells too at border=0 — the inter-column slot doubles as the
|
|
956
|
+
// leading-gutter slot of col[i].
|
|
957
|
+
if (border !== 0 || (!glyphs.wrapRightBorder && i > 0)) {
|
|
958
|
+
out += firstLine ? ' ' : glyphs.headerNlLeft;
|
|
959
|
+
}
|
|
960
|
+
// Header cells are always centered and padded to full width
|
|
961
|
+
// (upstream emits `%-*s%s%-*s` regardless of position — no
|
|
962
|
+
// skip-padding-on-last-cell quirk on header rows).
|
|
963
|
+
out += padToWidth(cells[i], widths[i], 'center');
|
|
964
|
+
// Trailing marker. Upstream `print.c` line 1003-1005:
|
|
965
|
+
// if (opt_border != 0 || format->wrap_right_border)
|
|
966
|
+
// fputs(!header_done[i] ? format->header_nl_right : " ", fout);
|
|
967
|
+
// For ASCII this is always emitted (wrap_right_border=true), so
|
|
968
|
+
// multi-line headers get `+` between content and the column
|
|
969
|
+
// separator AND on the trailing edge of the last column. For
|
|
970
|
+
// OLD-ASCII (wrap_right_border=false) this slot is skipped at
|
|
971
|
+
// border=0 — the inter-column space comes from the NEXT cell's
|
|
972
|
+
// leading-gutter emit on the next loop iteration.
|
|
973
|
+
if (border !== 0 || glyphs.wrapRightBorder) {
|
|
974
|
+
out += cellWrapNext[i] !== 'none' ? glyphs.headerNlRight : ' ';
|
|
975
|
+
}
|
|
976
|
+
// Column divider (not on the last column). For border 0 ASCII the
|
|
977
|
+
// trailing-marker slot above already consumes the inter-column gap.
|
|
978
|
+
// For border 0 OLD-ASCII the next iteration emits the leading-gutter
|
|
979
|
+
// slot, so no extra space is needed here either.
|
|
980
|
+
if (!isLast && border !== 0) {
|
|
981
|
+
out += vrule;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (border === 2 || border === 3)
|
|
985
|
+
out += vrule;
|
|
986
|
+
return out;
|
|
987
|
+
};
|
|
988
|
+
/**
|
|
989
|
+
* Render one display-line of a data row.
|
|
990
|
+
*
|
|
991
|
+
* Mirrors upstream `print_aligned_text` (print.c lines 1047-1180): one
|
|
992
|
+
* loop body, per-column emit `[left-marker] <content> [right-marker]
|
|
993
|
+
* [column-divider]`, then `[right-border]`. Padding is applied when:
|
|
994
|
+
* - the cell is not the last column (finalspaces=true in upstream),
|
|
995
|
+
* - OR `wrap[j]` is set (cell continues, so marker needs an aligned
|
|
996
|
+
* position).
|
|
997
|
+
*
|
|
998
|
+
* Right-aligned columns always pad on the left (spaces first), but per
|
|
999
|
+
* upstream they get a trailing right-margin pad only at border=2 (the
|
|
1000
|
+
* `finalspaces` branch). For border 0/1 right-aligned columns we follow
|
|
1001
|
+
* the same rule: padding before content, no trailing pad on the last
|
|
1002
|
+
* column when there's no continuation.
|
|
1003
|
+
*/
|
|
1004
|
+
const renderDataLine = (cells, widths, aligns, border, glyphs,
|
|
1005
|
+
// For each column, the "wrap state" carried over from the previous
|
|
1006
|
+
// physical line (drives the LEFT marker — `wrap[j]` in upstream's
|
|
1007
|
+
// pre-content fputs).
|
|
1008
|
+
cellWrapPrev,
|
|
1009
|
+
// For each column, the wrap state determined by THIS line's content
|
|
1010
|
+
// and whatever follows on the NEXT line (drives the RIGHT marker and
|
|
1011
|
+
// the column divider glyph).
|
|
1012
|
+
cellWrapNext,
|
|
1013
|
+
// True only on the first display line of a row. Used by old-ascii to
|
|
1014
|
+
// keep the regular `|` separator on the row's first line (the alt
|
|
1015
|
+
// midvrules only kick in once at least one continuation has been
|
|
1016
|
+
// emitted).
|
|
1017
|
+
firstLine) => {
|
|
1018
|
+
const { vrule } = glyphs;
|
|
1019
|
+
let out = '';
|
|
1020
|
+
if (border === 2 || border === 3)
|
|
1021
|
+
out += vrule;
|
|
1022
|
+
for (let i = 0; i < cells.length; i++) {
|
|
1023
|
+
const isLast = i === cells.length - 1;
|
|
1024
|
+
// Left marker. Border 0 has no leading-gutter slot for non-first
|
|
1025
|
+
// cells under ASCII (wrap_right_border=true); under OLD-ASCII the
|
|
1026
|
+
// leading-gutter slot doubles as the inter-column space for i > 0
|
|
1027
|
+
// (upstream `print.c` line 1066-1073).
|
|
1028
|
+
if (border !== 0) {
|
|
1029
|
+
out += dataLeftMarker(cellWrapPrev[i], glyphs);
|
|
1030
|
+
}
|
|
1031
|
+
else if (!glyphs.wrapRightBorder && i > 0) {
|
|
1032
|
+
out += dataLeftMarker(cellWrapPrev[i], glyphs);
|
|
1033
|
+
}
|
|
1034
|
+
// Decide whether to pad this cell. Upstream `print.c` lines 1141-1148:
|
|
1035
|
+
// left-aligned cells pad iff `finalspaces || wrap[j] != NONE`.
|
|
1036
|
+
// right-aligned cells always pad on the left side regardless.
|
|
1037
|
+
// `finalspaces` = (border == 2 || not last column).
|
|
1038
|
+
const finalspaces = border === 2 || border === 3 || !isLast;
|
|
1039
|
+
const needsTrailingPad = finalspaces || cellWrapNext[i] !== 'none';
|
|
1040
|
+
if (aligns[i] === 'right') {
|
|
1041
|
+
// Right-aligned cells always get full padding on the left so
|
|
1042
|
+
// content right-aligns. Trailing pad only when finalspaces (or
|
|
1043
|
+
// wrap, to keep marker aligned).
|
|
1044
|
+
out += padToWidth(cells[i], widths[i], 'right');
|
|
1045
|
+
}
|
|
1046
|
+
else if (needsTrailingPad) {
|
|
1047
|
+
out += padToWidth(cells[i], widths[i], 'left');
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
// Last cell, no wrap state — emit raw content (no trailing pad).
|
|
1051
|
+
out += cells[i];
|
|
1052
|
+
}
|
|
1053
|
+
// Right marker. Upstream `print.c` lines 1150-1156:
|
|
1054
|
+
// - WRAP → wrap_right
|
|
1055
|
+
// - NEWLINE→ nl_right
|
|
1056
|
+
// - NONE → space (only when not last col / border=2)
|
|
1057
|
+
// For OLD-ASCII at border 0 the inter-column trailing slot of
|
|
1058
|
+
// non-last cells is owned by the *next* iteration's leading-gutter
|
|
1059
|
+
// emit (single space either way). For the very last column, upstream
|
|
1060
|
+
// still emits a trailing char when the cell is on a continuation
|
|
1061
|
+
// (`cellWrapNext != none`) so wrapped cells keep their trailing
|
|
1062
|
+
// column position aligned with the first line.
|
|
1063
|
+
if (border !== 0 || glyphs.wrapRightBorder) {
|
|
1064
|
+
out += dataRightMarker(cellWrapNext[i], glyphs, isLast, border);
|
|
1065
|
+
}
|
|
1066
|
+
else if (isLast && cellWrapNext[i] !== 'none') {
|
|
1067
|
+
out += dataRightMarker(cellWrapNext[i], glyphs, isLast, border);
|
|
1068
|
+
}
|
|
1069
|
+
// Column divider. Upstream lines 1158-1169: in old-ascii the
|
|
1070
|
+
// midvrule between col[i] and col[i+1] swaps to a continuation
|
|
1071
|
+
// glyph when *either* adjacent cell is on a wrap/nl continuation
|
|
1072
|
+
// (midvrule_nl=":", midvrule_wrap=";", midvrule_blank=" "). For
|
|
1073
|
+
// ASCII / Unicode all three midvrule_* equal the regular vrule, so
|
|
1074
|
+
// the branch collapses to `vrule`.
|
|
1075
|
+
if (!isLast && border !== 0) {
|
|
1076
|
+
out += pickMidvrule(glyphs, cellWrapPrev[i], cellWrapPrev[i + 1], firstLine);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (border === 2 || border === 3)
|
|
1080
|
+
out += vrule;
|
|
1081
|
+
return out;
|
|
1082
|
+
};
|
|
1083
|
+
/**
|
|
1084
|
+
* Break a single line of text into chunks of at most `width` visible
|
|
1085
|
+
* columns. Greedy, code-point-aware. Used by wrapped mode and by
|
|
1086
|
+
* vertical mode for value wrapping.
|
|
1087
|
+
*/
|
|
1088
|
+
const wrapLine = (line, width) => {
|
|
1089
|
+
if (width <= 0)
|
|
1090
|
+
return [line];
|
|
1091
|
+
if (displayWidth(line) <= width)
|
|
1092
|
+
return [line];
|
|
1093
|
+
const chunks = [];
|
|
1094
|
+
let buf = '';
|
|
1095
|
+
let bufW = 0;
|
|
1096
|
+
for (const ch of line) {
|
|
1097
|
+
const cw = codePointWidth(ch.codePointAt(0) ?? 0);
|
|
1098
|
+
if (bufW + cw > width && buf.length > 0) {
|
|
1099
|
+
chunks.push(buf);
|
|
1100
|
+
buf = '';
|
|
1101
|
+
bufW = 0;
|
|
1102
|
+
}
|
|
1103
|
+
buf += ch;
|
|
1104
|
+
bufW += cw;
|
|
1105
|
+
}
|
|
1106
|
+
if (buf.length > 0)
|
|
1107
|
+
chunks.push(buf);
|
|
1108
|
+
return chunks;
|
|
1109
|
+
};
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
// Vertical / expanded rendering.
|
|
1112
|
+
// ---------------------------------------------------------------------------
|
|
1113
|
+
const renderVertical = (rs, opts) => {
|
|
1114
|
+
const topt = opts.topt;
|
|
1115
|
+
const border = topt.border;
|
|
1116
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
1117
|
+
const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
|
|
1118
|
+
const glyphs = glyphsFor(topt.unicodeBorderLineStyle);
|
|
1119
|
+
// `old-ascii` swaps several emission decisions (leading-slot at border<2,
|
|
1120
|
+
// suppressed data trailing marker, alternate midvrule glyph on
|
|
1121
|
+
// continuation lines). Track once at the top so the per-line code stays
|
|
1122
|
+
// readable.
|
|
1123
|
+
const oldAscii = topt.unicodeBorderLineStyle === 'old-ascii';
|
|
1124
|
+
const headers = rs.fields.map((f) => f.name);
|
|
1125
|
+
// Width of the name column = max header line width (multi-line headers
|
|
1126
|
+
// count per-line; upstream `pg_wcssize` returns the widest line).
|
|
1127
|
+
let nameWidth = 0;
|
|
1128
|
+
let hmultiline = false;
|
|
1129
|
+
for (const h of headers) {
|
|
1130
|
+
for (const line of h.split('\n')) {
|
|
1131
|
+
const w = displayWidth(line);
|
|
1132
|
+
if (w > nameWidth)
|
|
1133
|
+
nameWidth = w;
|
|
1134
|
+
}
|
|
1135
|
+
if (h.includes('\n'))
|
|
1136
|
+
hmultiline = true;
|
|
1137
|
+
}
|
|
1138
|
+
// Width of the value column = max value width across all rows.
|
|
1139
|
+
let valueWidth = 0;
|
|
1140
|
+
let dmultiline = false;
|
|
1141
|
+
const cellGrid = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
|
|
1142
|
+
for (const row of cellGrid) {
|
|
1143
|
+
for (const v of row) {
|
|
1144
|
+
if (v.includes('\n'))
|
|
1145
|
+
dmultiline = true;
|
|
1146
|
+
for (const line of v.split('\n')) {
|
|
1147
|
+
const w = displayWidth(line);
|
|
1148
|
+
if (w > valueWidth)
|
|
1149
|
+
valueWidth = w;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// Compute dwidth — the value-column width used for both the record
|
|
1154
|
+
// header padding and the per-cell value wrap. Mirrors upstream
|
|
1155
|
+
// `print_aligned_vertical` in `fe_utils/print.c` lines 1463-1583: the
|
|
1156
|
+
// value column shrinks to fit `output_columns` (with a hard floor
|
|
1157
|
+
// enforced via `min_width` and `rwidth`) when `\pset format wrapped`
|
|
1158
|
+
// AND `\pset columns N` is in effect. Runs for all borders 0/1/2.
|
|
1159
|
+
//
|
|
1160
|
+
// The `swidth` table below mirrors the upstream branches:
|
|
1161
|
+
// border 0: 1 (gutter) +1 if hmultiline
|
|
1162
|
+
// +1 if dmultiline (border<2 && !oldAscii)
|
|
1163
|
+
// border 1: 3 (` | `) +1 if hmultiline && oldAscii (left newline marker)
|
|
1164
|
+
// +1 if dmultiline (border<2 && !oldAscii)
|
|
1165
|
+
// border 2: 7 (outer vrules + spacers; no dmultiline bump)
|
|
1166
|
+
//
|
|
1167
|
+
// `rwidth` is the natural label width (`* RECORD N` + digit count).
|
|
1168
|
+
// The two-pass loop turns `dmultiline` on the first iteration if a
|
|
1169
|
+
// wrap is needed, then recomputes with the bumped swidth. We
|
|
1170
|
+
// implement that as a single pass over the two possible swidths.
|
|
1171
|
+
const wrappedMode = topt.format === 'wrapped';
|
|
1172
|
+
let dwidth = valueWidth;
|
|
1173
|
+
if (wrappedMode) {
|
|
1174
|
+
const outputColumns = topt.columns > 0 ? topt.columns : topt.envColumns;
|
|
1175
|
+
if (outputColumns > 0) {
|
|
1176
|
+
let baseSwidth;
|
|
1177
|
+
if (border === 0)
|
|
1178
|
+
baseSwidth = 1 + (hmultiline ? 1 : 0);
|
|
1179
|
+
else if (border === 1)
|
|
1180
|
+
baseSwidth = 3 + (hmultiline && oldAscii ? 1 : 0);
|
|
1181
|
+
else
|
|
1182
|
+
baseSwidth = 7;
|
|
1183
|
+
// dmultiline adds a marker column only when border < 2 and not old-ascii
|
|
1184
|
+
// (old-ascii suppresses the data trailing marker entirely at border<2).
|
|
1185
|
+
const dmAddOk = border < 2 && !oldAscii;
|
|
1186
|
+
const swidthInit = baseSwidth + (dmAddOk && dmultiline ? 1 : 0);
|
|
1187
|
+
const swidthAfterWrap = baseSwidth + (dmAddOk ? 1 : 0);
|
|
1188
|
+
// rwidth = label-width floor: `* RECORD N`(9), `-[ RECORD ]`(12),
|
|
1189
|
+
// or `+-[ RECORD ]-+`(15), each plus digit count for N.
|
|
1190
|
+
const labelOverhead = border === 0 ? 9 : border === 1 ? 12 : 15;
|
|
1191
|
+
const nrows = cellGrid.length;
|
|
1192
|
+
const rwidth = labelOverhead +
|
|
1193
|
+
(nrows > 0 ? 1 + Math.floor(Math.log10(Math.max(1, nrows))) : 0);
|
|
1194
|
+
const compute = (swidth) => {
|
|
1195
|
+
let width = nameWidth + swidth + valueWidth;
|
|
1196
|
+
if (width < rwidth)
|
|
1197
|
+
width = rwidth;
|
|
1198
|
+
let minWidth = nameWidth + swidth + 3;
|
|
1199
|
+
if (minWidth < rwidth)
|
|
1200
|
+
minWidth = rwidth;
|
|
1201
|
+
if (outputColumns >= width)
|
|
1202
|
+
return width - nameWidth - swidth;
|
|
1203
|
+
if (outputColumns < minWidth)
|
|
1204
|
+
return minWidth - nameWidth - swidth;
|
|
1205
|
+
return outputColumns - nameWidth - swidth;
|
|
1206
|
+
};
|
|
1207
|
+
// First pass with the natural swidth.
|
|
1208
|
+
let newDwidth = compute(swidthInit);
|
|
1209
|
+
// If wrap is needed (newDwidth < natural) AND dmultiline wasn't
|
|
1210
|
+
// already true AND we're at border<2, upstream toggles dmultiline
|
|
1211
|
+
// on and re-runs with swidth+1.
|
|
1212
|
+
if (newDwidth < valueWidth && !dmultiline && dmAddOk) {
|
|
1213
|
+
newDwidth = compute(swidthAfterWrap);
|
|
1214
|
+
dmultiline = true;
|
|
1215
|
+
}
|
|
1216
|
+
// Clamp: never grow the data column beyond the natural value
|
|
1217
|
+
// width (matches `dwidth = newdwidth` in upstream).
|
|
1218
|
+
dwidth = newDwidth < valueWidth ? newDwidth : valueWidth;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
let out = '';
|
|
1222
|
+
const newline = '\n';
|
|
1223
|
+
if (!tuplesOnly && topt.title) {
|
|
1224
|
+
out += topt.title + newline;
|
|
1225
|
+
}
|
|
1226
|
+
if (rs.rows.length === 0) {
|
|
1227
|
+
// Expanded mode on empty result: upstream emits the "(0 rows)"
|
|
1228
|
+
// footer (same wording as horizontal mode) followed by the trailing
|
|
1229
|
+
// blank-line separator, NOT a special "(No rows)" string. Verified
|
|
1230
|
+
// against vanilla psql 18: `\\pset expanded on` then `select 1
|
|
1231
|
+
// where false;` emits `(0 rows)\n\n`.
|
|
1232
|
+
if (!tuplesOnly)
|
|
1233
|
+
out += '(0 rows)' + newline;
|
|
1234
|
+
if (!tuplesOnly && opts.footers) {
|
|
1235
|
+
for (const f of opts.footers)
|
|
1236
|
+
out += f + newline;
|
|
1237
|
+
}
|
|
1238
|
+
if (!tuplesOnly)
|
|
1239
|
+
out += newline;
|
|
1240
|
+
return out;
|
|
1241
|
+
}
|
|
1242
|
+
// Decide whether multi-line markers should be emitted on header/data
|
|
1243
|
+
// continuations. Mirrors upstream `print_aligned_vertical` predicates
|
|
1244
|
+
// at print.c lines 1679-1689 (header right marker) and 1747-1763 (data
|
|
1245
|
+
// trailing marker):
|
|
1246
|
+
// non-old-ascii: header marker iff `border > 0 || hmultiline`
|
|
1247
|
+
// data marker iff `border > 1 || dmultiline`
|
|
1248
|
+
// old-ascii: header right marker emitted iff `border > 0`
|
|
1249
|
+
// data right marker emitted iff `border > 1`
|
|
1250
|
+
// (old-ascii relies on header_nl_LEFT for header
|
|
1251
|
+
// continuations and never emits the data right marker
|
|
1252
|
+
// at border<2 — it uses the alt midvrule glyph instead.)
|
|
1253
|
+
const emitHeaderMarker = oldAscii ? border > 0 : border > 0 || hmultiline;
|
|
1254
|
+
const emitDataMarker = oldAscii ? border > 1 : border > 1 || dmultiline;
|
|
1255
|
+
// Leading-slot for header column. Upstream `print_aligned_vertical`
|
|
1256
|
+
// line 1655-1657 emits the slot at `border==2 || (hmultiline &&
|
|
1257
|
+
// oldAscii)`. Old-ascii routes the continuation marker through the
|
|
1258
|
+
// leading gutter (`+` in `headerNlLeft`) instead of the trailing slot.
|
|
1259
|
+
const emitHeaderLeftSlot = border > 1 || (hmultiline && oldAscii);
|
|
1260
|
+
// Record-header label width. Upstream line 1610-1613 bumps `lhwidth`
|
|
1261
|
+
// by 1 at border<2 with hmultiline && oldAscii — the extra column is
|
|
1262
|
+
// the same `+` newline-indicator slot the data lines reserve.
|
|
1263
|
+
const lhwidth = border < 2 && hmultiline && oldAscii ? nameWidth + 1 : nameWidth;
|
|
1264
|
+
for (let r = 0; r < cellGrid.length; r++) {
|
|
1265
|
+
if (!tuplesOnly) {
|
|
1266
|
+
out += renderRecordHeader(r + 1, lhwidth, dwidth, border, glyphs, r === 0);
|
|
1267
|
+
out += newline;
|
|
1268
|
+
}
|
|
1269
|
+
else if (r > 0 || border === 2 || border === 3) {
|
|
1270
|
+
// tuples_only: upstream `print_aligned_vertical` line 1619-1621
|
|
1271
|
+
// still emits the inter-record separator (with the `* Record N`
|
|
1272
|
+
// label suppressed) for r>0, and the top rule at border=2/3 for
|
|
1273
|
+
// the very first record. Reuses the same record-header path with
|
|
1274
|
+
// `record=0` to suppress the label glyph.
|
|
1275
|
+
out += renderRecordHeader(0, lhwidth, dwidth, border, glyphs, r === 0);
|
|
1276
|
+
out += newline;
|
|
1277
|
+
}
|
|
1278
|
+
for (let c = 0; c < headers.length; c++) {
|
|
1279
|
+
const headerLines = headers[c].split('\n');
|
|
1280
|
+
const dataLines = cellGrid[r][c].split('\n');
|
|
1281
|
+
out += renderVerticalCell(headerLines, dataLines, nameWidth, dwidth, border, glyphs, hmultiline, emitHeaderMarker, emitDataMarker, emitHeaderLeftSlot, oldAscii);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (!tuplesOnly && (border === 2 || border === 3)) {
|
|
1285
|
+
out +=
|
|
1286
|
+
glyphs.botLeft +
|
|
1287
|
+
glyphs.hrule.repeat(nameWidth + 2) +
|
|
1288
|
+
glyphs.botMid +
|
|
1289
|
+
glyphs.hrule.repeat(dwidth + 2) +
|
|
1290
|
+
glyphs.botRight +
|
|
1291
|
+
newline;
|
|
1292
|
+
}
|
|
1293
|
+
// Footer + trailing blank-line handling mirrors upstream
|
|
1294
|
+
// `print_aligned_vertical` (print.c lines 1804-1822):
|
|
1295
|
+
// - At border < 2, emit a blank line BEFORE the user footers
|
|
1296
|
+
// (separates the last data line from the footer block).
|
|
1297
|
+
// - At border >= 2, the bottom rule already provides the separator
|
|
1298
|
+
// so no extra blank is emitted before footers.
|
|
1299
|
+
// - Always emit a trailing blank line at the very end —
|
|
1300
|
+
// `fputc('\n', fout)` on line 1821 is UNCONDITIONAL (not gated by
|
|
1301
|
+
// `opt_tuples_only`), so tuples_only mode still gets a single
|
|
1302
|
+
// separator before the next command.
|
|
1303
|
+
// - Expanded mode never emits a `(N rows)` default footer; the
|
|
1304
|
+
// trailing blank is the only output between consecutive results.
|
|
1305
|
+
const hasUserFooters = !!opts.footers && opts.footers.length > 0;
|
|
1306
|
+
if (!tuplesOnly && hasUserFooters && border < 2) {
|
|
1307
|
+
out += newline;
|
|
1308
|
+
}
|
|
1309
|
+
if (!tuplesOnly && opts.footers) {
|
|
1310
|
+
for (const f of opts.footers)
|
|
1311
|
+
out += f + newline;
|
|
1312
|
+
}
|
|
1313
|
+
out += newline;
|
|
1314
|
+
return out;
|
|
1315
|
+
};
|
|
1316
|
+
/**
|
|
1317
|
+
* Render the `[ RECORD N ]` block header for vertical mode.
|
|
1318
|
+
*
|
|
1319
|
+
* border 0: `* Record N<spaces-to-fill>` (padded to `nameWidth + valueWidth`)
|
|
1320
|
+
* border 1: `-[ RECORD N ]<dashes-to-fit>+<dashes-to-fit>`
|
|
1321
|
+
* border 2: `+-[ RECORD N ]<dashes>+<dashes>+`
|
|
1322
|
+
*
|
|
1323
|
+
* Mirrors psql `print_aligned_vertical_line`.
|
|
1324
|
+
*
|
|
1325
|
+
* Layout invariants (verified empirically against vanilla psql 18):
|
|
1326
|
+
*
|
|
1327
|
+
* border 1 — data line is `<name padded to nameWidth> | <value>`:
|
|
1328
|
+
* - The `|` column-separator sits at position `nameWidth + 2`.
|
|
1329
|
+
* - Record header `+` mid junction aligns with that `|`, so the
|
|
1330
|
+
* pre-junction (left) span is `nameWidth + 1` chars and the
|
|
1331
|
+
* post-junction (right) span is `valueWidth + 1` chars.
|
|
1332
|
+
* - When `1 + label.length` overflows the pre-junction span but the
|
|
1333
|
+
* label still fits in the full data row width, upstream pads with
|
|
1334
|
+
* hrule chars to the row width and OMITS the mid junction
|
|
1335
|
+
* (`-[ RECORD 1 ]--` for `nameWidth=2, valueWidth=10`).
|
|
1336
|
+
* - When `1 + label.length` overflows the row width too, upstream
|
|
1337
|
+
* emits just the label-prefix with no padding (the label IS the
|
|
1338
|
+
* whole divider line).
|
|
1339
|
+
*
|
|
1340
|
+
* border 2 — data line is `| <name> | <value> |`, row width is
|
|
1341
|
+
* `1 + (nameWidth + 2) + 1 + (valueWidth + 2) + 1`:
|
|
1342
|
+
* - Outer corners use the TOP glyph set on the first record
|
|
1343
|
+
* (`topLeft`, `topMid`, `topRight`) and the MID glyph set on
|
|
1344
|
+
* subsequent records (`midLeft`, `midMid`, `midRight`). The bottom
|
|
1345
|
+
* rule (emitted by `renderVertical`) uses the BOT glyphs.
|
|
1346
|
+
* - The `topMid` (or `midMid`) junction sits at position
|
|
1347
|
+
* `leftSegLen + 2` to match the `|` column-separator in the data
|
|
1348
|
+
* row. When `1 + label.length` overflows the left segment, the
|
|
1349
|
+
* junction glyph is dropped and the row is padded entirely with
|
|
1350
|
+
* hrule chars between the outer corners.
|
|
1351
|
+
*
|
|
1352
|
+
* border 0 — label is padded with spaces to `nameWidth + valueWidth`.
|
|
1353
|
+
* Labels that exceed `nameWidth + valueWidth` pass through unchanged.
|
|
1354
|
+
*
|
|
1355
|
+
* `isFirst` only affects border 2 glyph selection; border 0 and 1 share
|
|
1356
|
+
* a single set of single-line rule glyphs across all records.
|
|
1357
|
+
*/
|
|
1358
|
+
const renderRecordHeader = (record, nameWidth, valueWidth, border, glyphs, isFirst) => {
|
|
1359
|
+
const { hrule } = glyphs;
|
|
1360
|
+
// `record === 0` is the tuples_only "separator-only" sentinel — upstream
|
|
1361
|
+
// `print_aligned_vertical_line` (print.c line 1244-1249) gates the
|
|
1362
|
+
// label emission on `if (record)`, so passing 0 yields a bare rule
|
|
1363
|
+
// with no `* Record N` / `[ RECORD N ]` text. At border=0 that's a
|
|
1364
|
+
// blank line padded to width; at border>=1 a continuous hrule run.
|
|
1365
|
+
if (record === 0) {
|
|
1366
|
+
if (border === 0) {
|
|
1367
|
+
return ' '.repeat(nameWidth + valueWidth);
|
|
1368
|
+
}
|
|
1369
|
+
if (border === 1) {
|
|
1370
|
+
const rowWidth = nameWidth + 3 + valueWidth;
|
|
1371
|
+
const leftSpan = nameWidth + 1;
|
|
1372
|
+
// Mid junction lands at the `|` position in data rows.
|
|
1373
|
+
return (hrule.repeat(leftSpan) +
|
|
1374
|
+
glyphs.midMid +
|
|
1375
|
+
hrule.repeat(rowWidth - leftSpan - 1));
|
|
1376
|
+
}
|
|
1377
|
+
// border 2/3.
|
|
1378
|
+
const outerLeft = isFirst ? glyphs.topLeft : glyphs.midLeft;
|
|
1379
|
+
const outerRight = isFirst ? glyphs.topRight : glyphs.midRight;
|
|
1380
|
+
const outerMid = isFirst ? glyphs.topMid : glyphs.midMid;
|
|
1381
|
+
return (outerLeft +
|
|
1382
|
+
hrule.repeat(nameWidth + 2) +
|
|
1383
|
+
outerMid +
|
|
1384
|
+
hrule.repeat(valueWidth + 2) +
|
|
1385
|
+
outerRight);
|
|
1386
|
+
}
|
|
1387
|
+
if (border === 0) {
|
|
1388
|
+
const label = `* Record ${String(record)}`;
|
|
1389
|
+
const target = nameWidth + valueWidth;
|
|
1390
|
+
if (label.length >= target)
|
|
1391
|
+
return label;
|
|
1392
|
+
return label + ' '.repeat(target - label.length);
|
|
1393
|
+
}
|
|
1394
|
+
const label = `[ RECORD ${String(record)} ]`;
|
|
1395
|
+
// border 1: `-[ RECORD N ]---+---------`
|
|
1396
|
+
if (border === 1) {
|
|
1397
|
+
// Data row width = nameWidth + 3 + valueWidth (`<name padded> | <value>`).
|
|
1398
|
+
// Pre-junction span = nameWidth + 1 (name col + gutter space). The
|
|
1399
|
+
// `+` mid junction lands at position `nameWidth + 2`, mirroring the
|
|
1400
|
+
// `|` in the data lines. Post-junction span = valueWidth + 1.
|
|
1401
|
+
const rowWidth = nameWidth + 3 + valueWidth;
|
|
1402
|
+
const leftSpan = nameWidth + 1;
|
|
1403
|
+
const rightSpan = valueWidth + 1;
|
|
1404
|
+
const left = hrule + label;
|
|
1405
|
+
if (left.length <= leftSpan) {
|
|
1406
|
+
// Label fits in the pre-junction span. Pad left to leftSpan, emit
|
|
1407
|
+
// the mid junction (`+`), then rightSpan hrules.
|
|
1408
|
+
const leftPadded = left + hrule.repeat(leftSpan - left.length);
|
|
1409
|
+
return leftPadded + glyphs.midMid + hrule.repeat(rightSpan);
|
|
1410
|
+
}
|
|
1411
|
+
if (left.length < rowWidth) {
|
|
1412
|
+
// Label overflows the pre-junction span but still fits within the
|
|
1413
|
+
// full row. Pad the label-prefix out to the row width with hrule
|
|
1414
|
+
// chars and DROP the mid junction (vanilla psql parity).
|
|
1415
|
+
return left + hrule.repeat(rowWidth - left.length);
|
|
1416
|
+
}
|
|
1417
|
+
// Label exceeds the full row width — emit just the label-prefix
|
|
1418
|
+
// (no padding, no junction).
|
|
1419
|
+
return left;
|
|
1420
|
+
}
|
|
1421
|
+
// border 2 / 3.
|
|
1422
|
+
//
|
|
1423
|
+
// Outer corners come from the TOP glyph set on the first record (so
|
|
1424
|
+
// the leading rule looks like a normal table top) and from the MID
|
|
1425
|
+
// glyph set on subsequent records (so it looks like an inter-row rule).
|
|
1426
|
+
// For ASCII these all collapse to `+`; the distinction matters for
|
|
1427
|
+
// Unicode (`┌`/`┐`/`┬` vs `├`/`┤`/`┼`).
|
|
1428
|
+
const outerLeft = isFirst ? glyphs.topLeft : glyphs.midLeft;
|
|
1429
|
+
const outerRight = isFirst ? glyphs.topRight : glyphs.midRight;
|
|
1430
|
+
const outerMid = isFirst ? glyphs.topMid : glyphs.midMid;
|
|
1431
|
+
// Data row width = 1 + (nameWidth + 2) + 1 + (valueWidth + 2) + 1.
|
|
1432
|
+
const leftSegLen = nameWidth + 2;
|
|
1433
|
+
const rightSegLen = valueWidth + 2;
|
|
1434
|
+
const leftCore = hrule + label;
|
|
1435
|
+
if (leftCore.length <= leftSegLen) {
|
|
1436
|
+
// Label fits in the left segment. Pad left to leftSegLen, emit the
|
|
1437
|
+
// top/mid junction, then rightSegLen hrules, then top/mid right
|
|
1438
|
+
// corner.
|
|
1439
|
+
const leftPadded = leftCore + hrule.repeat(leftSegLen - leftCore.length);
|
|
1440
|
+
return (outerLeft + leftPadded + outerMid + hrule.repeat(rightSegLen) + outerRight);
|
|
1441
|
+
}
|
|
1442
|
+
// Label overflows the left segment. Drop the mid junction and pad
|
|
1443
|
+
// hrules between the outer corners. When the natural row inner width
|
|
1444
|
+
// (= leftSegLen + 1 + rightSegLen) is itself smaller than the label
|
|
1445
|
+
// plus a single trailing hrule, the rule grows past the data-row
|
|
1446
|
+
// width so the label always has at least one `─`/`-` of breathing
|
|
1447
|
+
// room before the closing corner (verified against vanilla psql 18:
|
|
1448
|
+
// `\gx` on `SELECT 1 as one, 2 as two \pset border 2` emits
|
|
1449
|
+
// `┌─[ RECORD 1 ]─┐` — 14 inner chars vs the 9-char inner data row).
|
|
1450
|
+
const innerWidth = Math.max(leftSegLen + 1 + rightSegLen, leftCore.length + 1);
|
|
1451
|
+
return (outerLeft +
|
|
1452
|
+
leftCore +
|
|
1453
|
+
hrule.repeat(innerWidth - leftCore.length) +
|
|
1454
|
+
outerRight);
|
|
1455
|
+
};
|
|
1456
|
+
/**
|
|
1457
|
+
* Render one cell (header + value) of an expanded-mode record.
|
|
1458
|
+
*
|
|
1459
|
+
* Mirrors the per-cell loop in upstream `print_aligned_vertical`
|
|
1460
|
+
* (print.c lines 1636-1801). Each iteration of the outer loop emits ONE
|
|
1461
|
+
* physical output line containing both the header part and the data
|
|
1462
|
+
* part. Iteration continues until BOTH the header and data sides are
|
|
1463
|
+
* exhausted (so a 3-line header against a 1-line value still emits 3
|
|
1464
|
+
* lines, with the data side blank from line 2 onward).
|
|
1465
|
+
*
|
|
1466
|
+
* Layout per iteration (border=0):
|
|
1467
|
+
* <name padded to nameWidth><header_nl_right or space>
|
|
1468
|
+
* <" " or wrap_left><value chunk (padded to dwidth iff more follows)><wrap_right / nl_right>
|
|
1469
|
+
*
|
|
1470
|
+
* Layout per iteration (border=1):
|
|
1471
|
+
* <name padded to nameWidth><header_nl_right or space>
|
|
1472
|
+
* <vrule>
|
|
1473
|
+
* <" " or wrap_left><value chunk><wrap_right / nl_right or trailing space>
|
|
1474
|
+
*
|
|
1475
|
+
* Layout per iteration (border=2): adds outer vrules, always pads the
|
|
1476
|
+
* value column to dwidth, and always emits trailing markers.
|
|
1477
|
+
*
|
|
1478
|
+
* When header is done but data is not, the header side becomes pure
|
|
1479
|
+
* whitespace of width `hwidth + opt_border (+ 1 for border-0 hmultiline)`.
|
|
1480
|
+
* When data is done but header is not (because hheight > dheight), the
|
|
1481
|
+
* data side is just `\n` at border<2, or `<dwidth spaces> |` at border>=2.
|
|
1482
|
+
*/
|
|
1483
|
+
const renderVerticalCell = (headerLines, dataLines, nameWidth, dwidth, border, glyphs, hmultiline, emitHeaderMarker, emitDataMarker, emitHeaderLeftSlot, oldAscii) => {
|
|
1484
|
+
const { vrule } = glyphs;
|
|
1485
|
+
let out = '';
|
|
1486
|
+
const dataChunks = [];
|
|
1487
|
+
for (const src of dataLines) {
|
|
1488
|
+
if (dwidth > 0 && displayWidth(src) > dwidth) {
|
|
1489
|
+
const pieces = wrapLine(src, dwidth);
|
|
1490
|
+
const lastIdx = pieces.length - 1;
|
|
1491
|
+
pieces.forEach((piece, pi) => {
|
|
1492
|
+
dataChunks.push({
|
|
1493
|
+
text: piece,
|
|
1494
|
+
width: displayWidth(piece),
|
|
1495
|
+
isWrapStart: pi === 0,
|
|
1496
|
+
isWrapEnd: pi === lastIdx,
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
dataChunks.push({
|
|
1502
|
+
text: src,
|
|
1503
|
+
width: displayWidth(src),
|
|
1504
|
+
isWrapStart: true,
|
|
1505
|
+
isWrapEnd: true,
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
let hLine = 0;
|
|
1510
|
+
let dLine = 0;
|
|
1511
|
+
// `offset` mirrors upstream's `offset` variable (print.c line 1638): it
|
|
1512
|
+
// tracks how much of the current data line has been emitted so far. A
|
|
1513
|
+
// non-zero value at the start of an iteration means we're continuing a
|
|
1514
|
+
// wrap (drives midvrule_wrap on old-ascii separator picking). Upstream
|
|
1515
|
+
// updates `offset += bytes_to_output` even on the final chunk of a
|
|
1516
|
+
// non-empty cell, so the next iteration (when header is still
|
|
1517
|
+
// continuing) sees offset > 0.
|
|
1518
|
+
let offset = 0;
|
|
1519
|
+
let hcomplete = headerLines.length === 0;
|
|
1520
|
+
let dcomplete = dataChunks.length === 0;
|
|
1521
|
+
while (!hcomplete || !dcomplete) {
|
|
1522
|
+
// ---- Left border ----
|
|
1523
|
+
if (border === 2 || border === 3)
|
|
1524
|
+
out += vrule;
|
|
1525
|
+
// ---- Header part ----
|
|
1526
|
+
if (!hcomplete) {
|
|
1527
|
+
// Leading slot. Upstream `print.c` lines 1655-1657: emitted at
|
|
1528
|
+
// `border==2 || (hmultiline && oldAscii)`. Old-ascii routes the
|
|
1529
|
+
// continuation marker through the LEFT side via header_nl_left.
|
|
1530
|
+
if (emitHeaderLeftSlot) {
|
|
1531
|
+
out += hLine > 0 ? glyphs.headerNlLeft : ' ';
|
|
1532
|
+
}
|
|
1533
|
+
const text = headerLines[hLine];
|
|
1534
|
+
out += padToWidth(text, nameWidth, 'left');
|
|
1535
|
+
const hasMore = hLine + 1 < headerLines.length;
|
|
1536
|
+
if (hasMore) {
|
|
1537
|
+
if (emitHeaderMarker)
|
|
1538
|
+
out += glyphs.headerNlRight;
|
|
1539
|
+
hLine++;
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
if (emitHeaderMarker)
|
|
1543
|
+
out += ' ';
|
|
1544
|
+
hcomplete = true;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
else {
|
|
1548
|
+
// Header exhausted but data still has lines. Pad with
|
|
1549
|
+
// `nameWidth + border` spaces, +1 at border<2 if hmultiline &&
|
|
1550
|
+
// oldAscii (mirrors the lhwidth bump), +1 at border==0 if
|
|
1551
|
+
// hmultiline && !oldAscii (mirrors the extra trailing slot the
|
|
1552
|
+
// non-old-ascii branch carved out). Upstream print.c lines
|
|
1553
|
+
// 1693-1707.
|
|
1554
|
+
let swidth = nameWidth + border;
|
|
1555
|
+
if (border < 2 && hmultiline && oldAscii)
|
|
1556
|
+
swidth++;
|
|
1557
|
+
if (border === 0 && hmultiline && !oldAscii)
|
|
1558
|
+
swidth++;
|
|
1559
|
+
out += ' '.repeat(swidth);
|
|
1560
|
+
}
|
|
1561
|
+
// ---- Separator ----
|
|
1562
|
+
// Border > 0 emits the column rule. Upstream `print.c` 1710-1719
|
|
1563
|
+
// picks midvrule_wrap (`;` old-ascii) when offset != 0 (mid-wrap or
|
|
1564
|
+
// we just finished emitting a non-empty cell), midvrule (`|`) on the
|
|
1565
|
+
// very first data line, midvrule_nl (`:` old-ascii) on subsequent
|
|
1566
|
+
// data lines. For ASCII/Unicode all three resolve to the same vrule
|
|
1567
|
+
// glyph so the branch collapses; for old-ascii the continuation
|
|
1568
|
+
// glyph drives the visual distinction.
|
|
1569
|
+
if (border > 0) {
|
|
1570
|
+
if (offset !== 0)
|
|
1571
|
+
out += glyphs.midvruleWrap;
|
|
1572
|
+
else if (dLine === 0)
|
|
1573
|
+
out += vrule;
|
|
1574
|
+
else
|
|
1575
|
+
out += glyphs.midvruleNl;
|
|
1576
|
+
}
|
|
1577
|
+
// ---- Data part ----
|
|
1578
|
+
if (!dcomplete) {
|
|
1579
|
+
const chunk = dataChunks[dLine];
|
|
1580
|
+
// Leading slot: " " on the first chunk of a source line,
|
|
1581
|
+
// wrap_left on a wrap continuation. ALWAYS emitted (upstream
|
|
1582
|
+
// print.c line 1731 has no border guard).
|
|
1583
|
+
out += offset === 0 ? ' ' : glyphs.wrapLeft;
|
|
1584
|
+
out += chunk.text;
|
|
1585
|
+
// Mirror upstream's `offset += bytes_to_output`: bumped even on
|
|
1586
|
+
// the last chunk of a cell, so the next iteration (header still
|
|
1587
|
+
// continuing) sees offset > 0 and picks midvrule_wrap.
|
|
1588
|
+
offset += chunk.width;
|
|
1589
|
+
const isLastChunk = dLine === dataChunks.length - 1;
|
|
1590
|
+
const nextChunk = !isLastChunk ? dataChunks[dLine + 1] : null;
|
|
1591
|
+
let needsPad = false;
|
|
1592
|
+
let markerGlyph = '';
|
|
1593
|
+
if (nextChunk && !chunk.isWrapEnd) {
|
|
1594
|
+
// Wrap continuation: next chunk continues THIS source line.
|
|
1595
|
+
if (emitDataMarker) {
|
|
1596
|
+
needsPad = true;
|
|
1597
|
+
markerGlyph = glyphs.wrapRight;
|
|
1598
|
+
}
|
|
1599
|
+
dLine++;
|
|
1600
|
+
// Offset stays bumped — next chunk continues the same source line.
|
|
1601
|
+
}
|
|
1602
|
+
else if (nextChunk) {
|
|
1603
|
+
// Source-line boundary: next chunk starts a new data line.
|
|
1604
|
+
if (emitDataMarker) {
|
|
1605
|
+
needsPad = true;
|
|
1606
|
+
markerGlyph = glyphs.nlRight;
|
|
1607
|
+
}
|
|
1608
|
+
dLine++;
|
|
1609
|
+
offset = 0; // reset for new source line
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
// End of cell.
|
|
1613
|
+
if (border > 1) {
|
|
1614
|
+
// Border 2/3: pad to dwidth, trailing space, then vrule.
|
|
1615
|
+
needsPad = true;
|
|
1616
|
+
markerGlyph = ' ';
|
|
1617
|
+
}
|
|
1618
|
+
dcomplete = true;
|
|
1619
|
+
// Offset stays bumped — header-tail iterations should see
|
|
1620
|
+
// midvrule_wrap unless the cell was empty.
|
|
1621
|
+
}
|
|
1622
|
+
if (needsPad) {
|
|
1623
|
+
const pad = dwidth - chunk.width;
|
|
1624
|
+
if (pad > 0)
|
|
1625
|
+
out += ' '.repeat(pad);
|
|
1626
|
+
}
|
|
1627
|
+
out += markerGlyph;
|
|
1628
|
+
// ---- Right border ----
|
|
1629
|
+
if (border === 2 || border === 3)
|
|
1630
|
+
out += vrule;
|
|
1631
|
+
}
|
|
1632
|
+
else {
|
|
1633
|
+
// Data exhausted. Border<2 emits no further chars (a bare `\n`).
|
|
1634
|
+
// Border>=2 pads the value column then closes with right vrule
|
|
1635
|
+
// (upstream: `fprintf(fout, "%*s %s\n", dwidth, "", rightvrule)`
|
|
1636
|
+
// — note the TWO trailing spaces between the dwidth pad and rule).
|
|
1637
|
+
if (border === 2 || border === 3) {
|
|
1638
|
+
out += ' '.repeat(dwidth) + ' ' + vrule;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
out += '\n';
|
|
1642
|
+
}
|
|
1643
|
+
return out;
|
|
1644
|
+
};
|
|
1645
|
+
// ---------------------------------------------------------------------------
|
|
1646
|
+
// Mode selection.
|
|
1647
|
+
// ---------------------------------------------------------------------------
|
|
1648
|
+
const horizontalTotalWidth = (rs, topt) => {
|
|
1649
|
+
const widths = computeColumnWidths(rs, topt);
|
|
1650
|
+
const border = topt.border;
|
|
1651
|
+
const n = widths.length;
|
|
1652
|
+
// Match upstream `print.c` (lines 763-769) width_total:
|
|
1653
|
+
// border 0: n; border 1: 3n-1; border 2/3: 3n+1.
|
|
1654
|
+
let overhead;
|
|
1655
|
+
if (border === 0)
|
|
1656
|
+
overhead = n;
|
|
1657
|
+
else if (border === 1)
|
|
1658
|
+
overhead = n * 3 - (n > 0 ? 1 : 0);
|
|
1659
|
+
else
|
|
1660
|
+
overhead = n * 3 + 1;
|
|
1661
|
+
return overhead + widths.reduce((a, b) => a + b, 0);
|
|
1662
|
+
};
|
|
1663
|
+
const chooseExpanded = (rs, topt) => {
|
|
1664
|
+
if (topt.expanded === 'on')
|
|
1665
|
+
return true;
|
|
1666
|
+
if (topt.expanded === 'auto') {
|
|
1667
|
+
const maxColumns = topt.columns > 0 ? topt.columns : topt.envColumns;
|
|
1668
|
+
if (maxColumns <= 0)
|
|
1669
|
+
return false;
|
|
1670
|
+
return horizontalTotalWidth(rs, topt) > maxColumns;
|
|
1671
|
+
}
|
|
1672
|
+
return false;
|
|
1673
|
+
};
|
|
1674
|
+
// ---------------------------------------------------------------------------
|
|
1675
|
+
// Public printer.
|
|
1676
|
+
// ---------------------------------------------------------------------------
|
|
1677
|
+
export const alignedPrinter = {
|
|
1678
|
+
format: 'aligned',
|
|
1679
|
+
printQuery(rs, opts, out) {
|
|
1680
|
+
const expanded = chooseExpanded(rs, opts.topt);
|
|
1681
|
+
const wrapped = opts.topt.format === 'wrapped';
|
|
1682
|
+
let text;
|
|
1683
|
+
if (expanded) {
|
|
1684
|
+
text = renderVertical(rs, opts);
|
|
1685
|
+
}
|
|
1686
|
+
else if (rs.rows.length === 0) {
|
|
1687
|
+
// Empty result: header + (0 rows) only, regardless of border.
|
|
1688
|
+
text = renderEmpty(rs, opts);
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
text = renderHorizontal(rs, opts, wrapped);
|
|
1692
|
+
}
|
|
1693
|
+
out.write(text);
|
|
1694
|
+
return Promise.resolve();
|
|
1695
|
+
},
|
|
1696
|
+
};
|
|
1697
|
+
const renderEmpty = (rs, opts) => {
|
|
1698
|
+
// For an empty horizontal result, psql still prints the header row
|
|
1699
|
+
// and rule, then the (0 rows) footer.
|
|
1700
|
+
const topt = opts.topt;
|
|
1701
|
+
const tuplesOnly = topt.tuplesOnly;
|
|
1702
|
+
const border = topt.border;
|
|
1703
|
+
const glyphs = glyphsFor(topt.unicodeBorderLineStyle);
|
|
1704
|
+
const headerCells = rs.fields.map((f) => formatCell(f.name));
|
|
1705
|
+
const widths = headerCells.map((c) => c.width);
|
|
1706
|
+
let out = '';
|
|
1707
|
+
if (!tuplesOnly && topt.title) {
|
|
1708
|
+
// Centre the title above the table, matching the horizontal-path
|
|
1709
|
+
// logic (and upstream `print_aligned_text` `width_total` formula).
|
|
1710
|
+
// Falling through with no padding when title >= width keeps left-
|
|
1711
|
+
// alignment for very wide titles — `\d <wide-named-table>` typical.
|
|
1712
|
+
const overhead = border === 0
|
|
1713
|
+
? widths.length
|
|
1714
|
+
: border === 1
|
|
1715
|
+
? widths.length * 3 - (widths.length > 0 ? 1 : 0)
|
|
1716
|
+
: widths.length * 3 + 1;
|
|
1717
|
+
const widthTotal = overhead + widths.reduce((a, b) => a + b, 0);
|
|
1718
|
+
const titleW = displayWidth(topt.title);
|
|
1719
|
+
if (titleW >= widthTotal) {
|
|
1720
|
+
out += topt.title + '\n';
|
|
1721
|
+
}
|
|
1722
|
+
else {
|
|
1723
|
+
out += ' '.repeat((widthTotal - titleW) >> 1) + topt.title + '\n';
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (!tuplesOnly && (border === 2 || border === 3)) {
|
|
1727
|
+
out += buildRule(widths, border, glyphs, 'top') + '\n';
|
|
1728
|
+
}
|
|
1729
|
+
if (!tuplesOnly) {
|
|
1730
|
+
const noneStates = headerCells.map(() => 'none');
|
|
1731
|
+
out +=
|
|
1732
|
+
renderHeaderLine(headerCells.map((c) => c.lines[0] ?? ''), widths, border, glyphs, noneStates, noneStates, true) + '\n';
|
|
1733
|
+
out += buildRule(widths, border, glyphs, 'middle') + '\n';
|
|
1734
|
+
}
|
|
1735
|
+
if (!tuplesOnly && (border === 2 || border === 3)) {
|
|
1736
|
+
out += buildRule(widths, border, glyphs, 'bottom') + '\n';
|
|
1737
|
+
}
|
|
1738
|
+
// User footers suppress the default `(0 rows)` row counter — mirrors
|
|
1739
|
+
// upstream `footers_with_default` (print.c lines 397-413).
|
|
1740
|
+
const hasUserFooters = !!opts.footers && opts.footers.length > 0;
|
|
1741
|
+
if (!tuplesOnly && topt.defaultFooter && !hasUserFooters) {
|
|
1742
|
+
out += '(0 rows)\n';
|
|
1743
|
+
}
|
|
1744
|
+
if (!tuplesOnly && opts.footers) {
|
|
1745
|
+
for (const f of opts.footers)
|
|
1746
|
+
out += f + '\n';
|
|
1747
|
+
}
|
|
1748
|
+
// Trailing blank line between query results, mirroring the horizontal /
|
|
1749
|
+
// vertical code paths (which append `\n` via `printTableCleanup`).
|
|
1750
|
+
// Empty results were previously missing this separator, which made
|
|
1751
|
+
// multiple back-to-back `\d`-family queries run together. Emitted
|
|
1752
|
+
// unconditionally (matches `fputc('\n', fout)` on print.c line 1196 /
|
|
1753
|
+
// 1821 — both are outside the `!opt_tuples_only` guard).
|
|
1754
|
+
out += '\n';
|
|
1755
|
+
return out;
|
|
1756
|
+
};
|