neonctl 2.37.0 → 2.38.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.
Files changed (160) hide show
  1. package/{LICENSE.md → LICENSE} +27 -3
  2. package/README.md +15 -700
  3. package/bin/cli.js +3 -0
  4. package/package.json +8 -75
  5. package/dist/analytics.js +0 -168
  6. package/dist/api.js +0 -720
  7. package/dist/auth.js +0 -125
  8. package/dist/callback.html +0 -51
  9. package/dist/cli.js +0 -9
  10. package/dist/commands/api.js +0 -278
  11. package/dist/commands/auth.js +0 -214
  12. package/dist/commands/bootstrap.js +0 -481
  13. package/dist/commands/branches.js +0 -488
  14. package/dist/commands/bucket.js +0 -549
  15. package/dist/commands/checkout.js +0 -321
  16. package/dist/commands/config.js +0 -626
  17. package/dist/commands/connection_string.js +0 -172
  18. package/dist/commands/data_api.js +0 -284
  19. package/dist/commands/databases.js +0 -82
  20. package/dist/commands/deploy.js +0 -26
  21. package/dist/commands/dev.js +0 -698
  22. package/dist/commands/diff.js +0 -222
  23. package/dist/commands/env.js +0 -189
  24. package/dist/commands/functions.js +0 -373
  25. package/dist/commands/index.js +0 -62
  26. package/dist/commands/init.js +0 -73
  27. package/dist/commands/inspect.js +0 -65
  28. package/dist/commands/ip_allow.js +0 -137
  29. package/dist/commands/link.js +0 -1121
  30. package/dist/commands/neon_auth.js +0 -1028
  31. package/dist/commands/operations.js +0 -28
  32. package/dist/commands/orgs.js +0 -24
  33. package/dist/commands/projects.js +0 -413
  34. package/dist/commands/psql.js +0 -62
  35. package/dist/commands/roles.js +0 -65
  36. package/dist/commands/schema_diff.js +0 -151
  37. package/dist/commands/set_context.js +0 -29
  38. package/dist/commands/snapshots.js +0 -455
  39. package/dist/commands/status.js +0 -40
  40. package/dist/commands/user.js +0 -15
  41. package/dist/commands/vpc_endpoints.js +0 -134
  42. package/dist/config.js +0 -11
  43. package/dist/config_format.js +0 -72
  44. package/dist/context.js +0 -231
  45. package/dist/current_branch_fast_path.js +0 -55
  46. package/dist/dev/env.js +0 -244
  47. package/dist/dev/functions.js +0 -70
  48. package/dist/dev/inputs.js +0 -63
  49. package/dist/dev/runtime.js +0 -146
  50. package/dist/env.js +0 -36
  51. package/dist/env_file.js +0 -159
  52. package/dist/errors.js +0 -80
  53. package/dist/functions_api.js +0 -48
  54. package/dist/help.js +0 -146
  55. package/dist/index.js +0 -240
  56. package/dist/log.js +0 -18
  57. package/dist/parameters.gen.js +0 -480
  58. package/dist/pkg.js +0 -25
  59. package/dist/psql/cli.js +0 -53
  60. package/dist/psql/command/cmd_cond.js +0 -437
  61. package/dist/psql/command/cmd_connect.js +0 -820
  62. package/dist/psql/command/cmd_copy.js +0 -1035
  63. package/dist/psql/command/cmd_describe.js +0 -1815
  64. package/dist/psql/command/cmd_format.js +0 -948
  65. package/dist/psql/command/cmd_io.js +0 -2193
  66. package/dist/psql/command/cmd_lo.js +0 -393
  67. package/dist/psql/command/cmd_meta.js +0 -969
  68. package/dist/psql/command/cmd_misc.js +0 -187
  69. package/dist/psql/command/cmd_pipeline.js +0 -1148
  70. package/dist/psql/command/cmd_restrict.js +0 -171
  71. package/dist/psql/command/cmd_show.js +0 -766
  72. package/dist/psql/command/dispatch.js +0 -343
  73. package/dist/psql/command/inputQueue.js +0 -42
  74. package/dist/psql/command/shared.js +0 -71
  75. package/dist/psql/complete/filenames.js +0 -139
  76. package/dist/psql/complete/index.js +0 -104
  77. package/dist/psql/complete/matcher.js +0 -315
  78. package/dist/psql/complete/psqlVars.js +0 -249
  79. package/dist/psql/complete/queries.js +0 -493
  80. package/dist/psql/complete/rules.js +0 -2424
  81. package/dist/psql/core/common.js +0 -1253
  82. package/dist/psql/core/help.js +0 -576
  83. package/dist/psql/core/mainloop.js +0 -1360
  84. package/dist/psql/core/prompt.js +0 -439
  85. package/dist/psql/core/settings.js +0 -686
  86. package/dist/psql/core/sqlHelp.js +0 -1066
  87. package/dist/psql/core/startup.js +0 -846
  88. package/dist/psql/core/syncVars.js +0 -116
  89. package/dist/psql/core/variables.js +0 -287
  90. package/dist/psql/describe/formatters.js +0 -1330
  91. package/dist/psql/describe/processNamePattern.js +0 -270
  92. package/dist/psql/describe/queries.js +0 -2452
  93. package/dist/psql/describe/versionGate.js +0 -44
  94. package/dist/psql/index.js +0 -2030
  95. package/dist/psql/io/history.js +0 -299
  96. package/dist/psql/io/input.js +0 -120
  97. package/dist/psql/io/lineEditor/buffer.js +0 -325
  98. package/dist/psql/io/lineEditor/complete.js +0 -227
  99. package/dist/psql/io/lineEditor/filename.js +0 -159
  100. package/dist/psql/io/lineEditor/index.js +0 -893
  101. package/dist/psql/io/lineEditor/keymap.js +0 -745
  102. package/dist/psql/io/lineEditor/vt100.js +0 -363
  103. package/dist/psql/io/pgpass.js +0 -202
  104. package/dist/psql/io/pgservice.js +0 -194
  105. package/dist/psql/io/psqlrc.js +0 -422
  106. package/dist/psql/print/aligned.js +0 -1748
  107. package/dist/psql/print/asciidoc.js +0 -230
  108. package/dist/psql/print/crosstab.js +0 -463
  109. package/dist/psql/print/csv.js +0 -76
  110. package/dist/psql/print/html.js +0 -240
  111. package/dist/psql/print/json.js +0 -96
  112. package/dist/psql/print/latex.js +0 -379
  113. package/dist/psql/print/pager.js +0 -267
  114. package/dist/psql/print/troff.js +0 -240
  115. package/dist/psql/print/unaligned.js +0 -99
  116. package/dist/psql/print/units.js +0 -188
  117. package/dist/psql/scanner/slash.js +0 -515
  118. package/dist/psql/scanner/sql.js +0 -914
  119. package/dist/psql/scanner/stringutils.js +0 -394
  120. package/dist/psql/types/backslash.js +0 -1
  121. package/dist/psql/types/connection.js +0 -1
  122. package/dist/psql/types/index.js +0 -7
  123. package/dist/psql/types/printer.js +0 -1
  124. package/dist/psql/types/repl.js +0 -1
  125. package/dist/psql/types/scanner.js +0 -24
  126. package/dist/psql/types/settings.js +0 -1
  127. package/dist/psql/types/variables.js +0 -1
  128. package/dist/psql/wire/connection.js +0 -2857
  129. package/dist/psql/wire/copy.js +0 -108
  130. package/dist/psql/wire/notify.js +0 -59
  131. package/dist/psql/wire/pipeline.js +0 -521
  132. package/dist/psql/wire/protocol.js +0 -466
  133. package/dist/psql/wire/sasl.js +0 -294
  134. package/dist/psql/wire/tls.js +0 -602
  135. package/dist/storage_api.js +0 -147
  136. package/dist/test_utils/fixtures.js +0 -121
  137. package/dist/test_utils/oauth_server.js +0 -9
  138. package/dist/types.js +0 -1
  139. package/dist/utils/ai_gateway_notice.js +0 -180
  140. package/dist/utils/api_enums.js +0 -33
  141. package/dist/utils/auth.js +0 -5
  142. package/dist/utils/branch_notice.js +0 -22
  143. package/dist/utils/branch_picker.js +0 -103
  144. package/dist/utils/compute_units.js +0 -28
  145. package/dist/utils/config_diff.js +0 -185
  146. package/dist/utils/enrichers.js +0 -161
  147. package/dist/utils/esbuild.js +0 -158
  148. package/dist/utils/formats.js +0 -18
  149. package/dist/utils/git_diff.js +0 -90
  150. package/dist/utils/inspect_db.js +0 -153
  151. package/dist/utils/inspect_queries.js +0 -372
  152. package/dist/utils/middlewares.js +0 -20
  153. package/dist/utils/openapi.js +0 -114
  154. package/dist/utils/package_manager.js +0 -68
  155. package/dist/utils/point_in_time.js +0 -56
  156. package/dist/utils/psql.js +0 -120
  157. package/dist/utils/string.js +0 -5
  158. package/dist/utils/ui.js +0 -59
  159. package/dist/utils/zip.js +0 -4
  160. package/dist/writer.js +0 -97
@@ -1,1748 +0,0 @@
1
- import { boolDisplayOf, renderCellValue } 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], // Hangul Jamo
20
- [0x2329, 0x232a], // Angle brackets
21
- [0x2e80, 0x303e], // CJK Radicals, Kangxi, ...
22
- [0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, etc.
23
- [0x3400, 0x4dbf], // CJK Unified Ideographs Extension A
24
- [0x4e00, 0x9fff], // CJK Unified Ideographs
25
- [0xa000, 0xa4cf], // Yi Syllables
26
- [0xac00, 0xd7a3], // Hangul Syllables
27
- [0xf900, 0xfaff], // CJK Compatibility Ideographs
28
- [0xfe10, 0xfe19], // Vertical forms
29
- [0xfe30, 0xfe6f], // CJK Compatibility Forms, Small Form Variants
30
- [0xff00, 0xff60], // Fullwidth Forms
31
- [0xffe0, 0xffe6], // Fullwidth signs
32
- [0x1f300, 0x1f64f], // Misc Symbols & Pictographs, Emoticons
33
- [0x1f900, 0x1f9ff], // Supplemental Symbols & Pictographs
34
- [0x20000, 0x2fffd], // CJK Extension B..F
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], // Combining Diacritical Marks
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], // zero-width space, ZWNJ, ZWJ, LRM, RLM
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], // variation selectors
237
- [0xfe20, 0xfe2f], // combining half-marks
238
- [0xfeff, 0xfeff], // BOM
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, // int8
346
- 21, // int2
347
- 23, // int4
348
- 26, // oid
349
- 28, // xid
350
- 29, // cid
351
- 700, // float4
352
- 701, // float8
353
- 790, // money (cash)
354
- 1700, // numeric
355
- ]);
356
- const isRightAlignedField = (oid) => RIGHT_ALIGNED_OIDS.has(oid);
357
- const renderCell = (cell, nullPrint, numericLocale, dataTypeID, boolDisplay) => renderCellValue(cell, nullPrint, numericLocale, dataTypeID, boolDisplay);
358
- const ASCII_GLYPHS = {
359
- hrule: "-",
360
- vrule: "|",
361
- topLeft: "+",
362
- topMid: "+",
363
- topRight: "+",
364
- midLeft: "+",
365
- midMid: "+",
366
- midRight: "+",
367
- botLeft: "+",
368
- botMid: "+",
369
- botRight: "+",
370
- headerNlLeft: " ",
371
- headerNlRight: "+",
372
- wrapLeft: ".",
373
- wrapRight: ".",
374
- nlLeft: " ",
375
- nlRight: "+",
376
- wrapRightBorder: true,
377
- midvruleNl: "|",
378
- midvruleWrap: "|",
379
- midvruleBlank: "|",
380
- };
381
- // `pg_asciiformat_old` — the legacy ASCII renderer kept around for
382
- // `\pset linestyle old-ascii`. Two visible quirks vs the modern ASCII
383
- // glyphs:
384
- // - left-side `+` markers in headers (header_nl_left), no right-side
385
- // marker on either headers or data, and `wrap_right_border=false`
386
- // (no trailing marker at the table edge);
387
- // - the column separator on continuation rows changes — `:` when the
388
- // joining cell has more `\n`-split lines below, `;` when it has more
389
- // wrap-split lines, ` ` when the cell is exhausted (`midvrule_blank`).
390
- const OLD_ASCII_GLYPHS = {
391
- hrule: "-",
392
- vrule: "|",
393
- topLeft: "+",
394
- topMid: "+",
395
- topRight: "+",
396
- midLeft: "+",
397
- midMid: "+",
398
- midRight: "+",
399
- botLeft: "+",
400
- botMid: "+",
401
- botRight: "+",
402
- headerNlLeft: "+",
403
- headerNlRight: " ",
404
- wrapLeft: " ",
405
- wrapRight: " ",
406
- nlLeft: " ",
407
- nlRight: " ",
408
- wrapRightBorder: false,
409
- midvruleNl: ":",
410
- midvruleWrap: ";",
411
- midvruleBlank: " ",
412
- };
413
- // Light box-drawing glyphs (the only Unicode variant we expose today —
414
- // `unicode_border_linestyle=double` / `unicode_column_linestyle=double` /
415
- // `unicode_header_linestyle=double` are parsed by `cmd_format.ts` but the
416
- // shared `Unicode2LineStyle` slot in `PrintTableOpts` only carries
417
- // `ascii | unicode`, so all three settings collapse onto these "single"
418
- // glyphs. Adding the double variant requires extending the type to keep
419
- // `single`/`double` distinct (or carrying a separate side-channel field) —
420
- // out of scope for this pass.
421
- //
422
- // Codepoints (verified against upstream `unicode_style` in print.c):
423
- // U+2500 ─ Box Drawings Light Horizontal (hrule)
424
- // U+2502 │ Box Drawings Light Vertical (vrule)
425
- // U+250C ┌ Box Drawings Light Down and Right (topLeft)
426
- // U+252C ┬ Box Drawings Light Down and Horizontal (topMid)
427
- // U+2510 ┐ Box Drawings Light Down and Left (topRight)
428
- // U+251C ├ Box Drawings Light Vertical and Right (midLeft)
429
- // U+253C ┼ Box Drawings Light Vertical and Horiz. (midMid)
430
- // U+2524 ┤ Box Drawings Light Vertical and Left (midRight)
431
- // U+2514 └ Box Drawings Light Up and Right (botLeft)
432
- // U+2534 ┴ Box Drawings Light Up and Horizontal (botMid)
433
- // U+2518 ┘ Box Drawings Light Up and Left (botRight)
434
- const UNICODE_GLYPHS = {
435
- hrule: "─",
436
- vrule: "│",
437
- topLeft: "┌",
438
- topMid: "┬",
439
- topRight: "┐",
440
- midLeft: "├",
441
- midMid: "┼",
442
- midRight: "┤",
443
- botLeft: "└",
444
- botMid: "┴",
445
- botRight: "┘",
446
- // U+21B5 ↵ "Downwards Arrow with Corner Leftwards" — newline marker.
447
- // U+2026 … "Horizontal Ellipsis" — in-cell wrap marker.
448
- // Matches `unicode_style` in upstream `fe_utils/print.c`.
449
- headerNlLeft: " ",
450
- headerNlRight: "↵",
451
- wrapLeft: "…",
452
- wrapRight: "…",
453
- nlLeft: " ",
454
- nlRight: "↵",
455
- wrapRightBorder: true,
456
- midvruleNl: "│",
457
- midvruleWrap: "│",
458
- midvruleBlank: "│",
459
- };
460
- const glyphsFor = (style) => {
461
- if (style === "unicode")
462
- return UNICODE_GLYPHS;
463
- if (style === "old-ascii")
464
- return OLD_ASCII_GLYPHS;
465
- return ASCII_GLYPHS;
466
- };
467
- // ---------------------------------------------------------------------------
468
- // Column width computation.
469
- // ---------------------------------------------------------------------------
470
- /**
471
- * Compute the per-column max-width array used by horizontal layout.
472
- * Considers header width and every line of every cell value. Returns
473
- * widths in display-character units.
474
- *
475
- * Cells are pre-rendered with `nullPrint` and `numericLocale` already
476
- * applied so width measurements match what the printer will emit.
477
- */
478
- export const computeColumnWidths = (rs, topt) => {
479
- const nullPrint = topt.nullPrint;
480
- const numericLocale = topt.numericLocale;
481
- const boolDisplay = boolDisplayOf(topt);
482
- const widths = rs.fields.map((f) => displayWidth(f.name));
483
- for (const row of rs.rows) {
484
- for (let i = 0; i < rs.fields.length; i++) {
485
- const cellText = renderCell(row[i], nullPrint, numericLocale, rs.fields[i]?.dataTypeID, boolDisplay);
486
- for (const line of cellText.split("\n")) {
487
- const w = displayWidth(line);
488
- if (w > widths[i])
489
- widths[i] = w;
490
- }
491
- }
492
- }
493
- return widths;
494
- };
495
- const formatCell = (text) => {
496
- const lines = text.split("\n");
497
- let width = 0;
498
- for (const l of lines) {
499
- const w = displayWidth(l);
500
- if (w > width)
501
- width = w;
502
- }
503
- return { lines, width };
504
- };
505
- /**
506
- * Build a horizontal rule line (top, middle, bottom). Layout matches
507
- * psql:
508
- *
509
- * border 0: `aa bb` -> `-- --` (no corners, single-space gaps)
510
- * border 1: `-aa-+-bb-` (no outer corners, '+' between columns)
511
- * border 2: `+-aa-+-bb-+` (full box)
512
- * border 3: same as 2 here; row rules between data rows added elsewhere.
513
- *
514
- * The hrule covers `width + 2` characters per column when border >= 1
515
- * (the content padding spaces become hrule chars on rule lines).
516
- */
517
- const buildRule = (widths, border, glyphs, position) => {
518
- const { hrule } = glyphs;
519
- let left;
520
- let mid;
521
- let right;
522
- if (position === "top") {
523
- left = glyphs.topLeft;
524
- mid = glyphs.topMid;
525
- right = glyphs.topRight;
526
- }
527
- else if (position === "middle") {
528
- left = glyphs.midLeft;
529
- mid = glyphs.midMid;
530
- right = glyphs.midRight;
531
- }
532
- else {
533
- left = glyphs.botLeft;
534
- mid = glyphs.botMid;
535
- right = glyphs.botRight;
536
- }
537
- let out = "";
538
- if (border === 2 || border === 3) {
539
- out += left;
540
- }
541
- for (let i = 0; i < widths.length; i++) {
542
- // Per column the rule covers content width plus the two side pads
543
- // that the data lines have for border >= 1.
544
- const pad = border === 0 ? 0 : 1;
545
- out += hrule.repeat(widths[i] + pad * 2);
546
- if (i < widths.length - 1) {
547
- if (border === 0) {
548
- out += " "; // single space between columns in border 0
549
- }
550
- else {
551
- out += mid;
552
- }
553
- }
554
- }
555
- if (border === 2 || border === 3) {
556
- out += right;
557
- }
558
- return out;
559
- };
560
- const renderHorizontal = (rs, opts, wrapped) => {
561
- const topt = opts.topt;
562
- const border = topt.border;
563
- const tuplesOnly = topt.tuplesOnly;
564
- const nullPrint = opts.nullPrint !== "" ? opts.nullPrint : topt.nullPrint;
565
- const glyphs = glyphsFor(topt.unicodeBorderLineStyle);
566
- const headers = rs.fields.map((f) => f.name);
567
- const aligns = rs.fields.map((f) => isRightAlignedField(f.dataTypeID) ? "right" : "left");
568
- // Pre-render & measure every cell.
569
- const boolDisplay = boolDisplayOf(topt);
570
- const cellGrid = rs.rows.map((row) => row.map((cell, i) => formatCell(renderCell(cell, nullPrint, topt.numericLocale, rs.fields[i]?.dataTypeID, boolDisplay))));
571
- const headerCells = headers.map((h) => formatCell(h));
572
- const colCount = rs.fields.length;
573
- const widths = headerCells.map((c) => c.width);
574
- for (const row of cellGrid) {
575
- for (let i = 0; i < colCount; i++) {
576
- if (row[i].width > widths[i])
577
- widths[i] = row[i].width;
578
- }
579
- }
580
- // Wrapped mode shrinks the worst column until total width fits
581
- // `topt.columns`. Mirrors upstream `print_aligned_text` in
582
- // `fe_utils/print.c`: we score each column by
583
- //
584
- // ratio = current_width / average_width + max_width * 0.01
585
- //
586
- // and shrink the column with the highest ratio. That picks the column
587
- // whose individual rows are mostly narrower than the worst-case row
588
- // (so wrapping costs fewer extra lines than shrinking a uniformly-wide
589
- // column would). The +max*0.01 bias breaks ties in favour of the
590
- // overall-widest column, matching the upstream comment "Slightly bias
591
- // against wider columns. (Increases chance a narrow column will fit
592
- // in its cell.)".
593
- //
594
- // `width_wrap` (== `widths` here) starts as `max_width`. Each loop
595
- // iteration decrements it by one until the total fits or no column
596
- // can shrink further (every column is already at its header width).
597
- if (wrapped) {
598
- const maxColumns = topt.columns > 0 ? topt.columns : topt.envColumns;
599
- if (maxColumns > 0) {
600
- // Average width per column over the data rows (header excluded,
601
- // matching upstream `width_average` which divides cell_count by
602
- // col_count). Each cell's width is its max line width (mirrors
603
- // upstream `pg_wcssize`, which returns the widest line of the
604
- // possibly-multiline cell).
605
- const widthAverage = new Array(colCount).fill(0);
606
- const maxWidth = headerCells.map((c) => c.width);
607
- if (cellGrid.length > 0) {
608
- for (const row of cellGrid) {
609
- for (let i = 0; i < colCount; i++) {
610
- widthAverage[i] += row[i].width;
611
- if (row[i].width > maxWidth[i])
612
- maxWidth[i] = row[i].width;
613
- }
614
- }
615
- for (let i = 0; i < colCount; i++) {
616
- widthAverage[i] /= cellGrid.length;
617
- }
618
- }
619
- // Total fixed overhead. Mirrors upstream `print.c` lines 763-769:
620
- // border 0: col_count (one trailing-marker slot per cell)
621
- // border 1: col_count * 3 - 1 (3 slots per cell minus shared sep)
622
- // border 2: col_count * 3 + 1 (3 slots per cell plus outer rules)
623
- // For ASCII / Unicode the trailing marker slot is always emitted
624
- // (wrap_right_border=true), so the formula matches verbatim.
625
- const overheadFor = (n) => {
626
- if (border === 0)
627
- return n;
628
- if (border === 1)
629
- return n * 3 - (n > 0 ? 1 : 0);
630
- return n * 3 + 1;
631
- };
632
- const overhead = overheadFor(colCount);
633
- const totalHeaderWidth = overhead + headerCells.reduce((a, c) => a + c.width, 0);
634
- let total = overhead + widths.reduce((a, b) => a + b, 0);
635
- // Upstream guards against shrinking when even the headers don't
636
- // fit — there's no point picking columns to wrap if the rule row
637
- // is already too wide.
638
- if (maxColumns >= totalHeaderWidth) {
639
- while (total > maxColumns) {
640
- let worstCol = -1;
641
- let maxRatio = 0;
642
- for (let i = 0; i < colCount; i++) {
643
- const headerW = headerCells[i].width;
644
- // Two preconditions from upstream:
645
- // - width_average[i] != 0 (column has some data; avoids /0)
646
- // - width_wrap[i] > width_header[i] (header is the floor;
647
- // can't shrink below it without overlapping the header).
648
- if (widthAverage[i] > 0 && widths[i] > headerW) {
649
- const ratio = widths[i] / widthAverage[i] +
650
- maxWidth[i] * 0.01;
651
- if (ratio > maxRatio) {
652
- maxRatio = ratio;
653
- worstCol = i;
654
- }
655
- }
656
- }
657
- if (worstCol === -1)
658
- break;
659
- widths[worstCol]--;
660
- total--;
661
- }
662
- }
663
- }
664
- }
665
- let out = "";
666
- const newline = "\n";
667
- // Compute total table width once: needed for title centring (and only
668
- // there at the moment). Mirrors upstream `width_total` from
669
- // print.c lines 935-950: when the title is narrower than the table,
670
- // centre it; otherwise left-align (no padding).
671
- let widthTotal = 0;
672
- if (!tuplesOnly && topt.title) {
673
- const overhead = border === 0
674
- ? widths.length
675
- : border === 1
676
- ? widths.length * 3 - (widths.length > 0 ? 1 : 0)
677
- : widths.length * 3 + 1;
678
- widthTotal = overhead + widths.reduce((a, b) => a + b, 0);
679
- const titleW = displayWidth(topt.title);
680
- if (titleW >= widthTotal) {
681
- out += topt.title + newline;
682
- }
683
- else {
684
- const pad = (widthTotal - titleW) >> 1;
685
- out += " ".repeat(pad) + topt.title + newline;
686
- }
687
- }
688
- // Top rule: emitted when border == 2 or 3.
689
- if (!tuplesOnly && (border === 2 || border === 3)) {
690
- out += buildRule(widths, border, glyphs, "top") + newline;
691
- }
692
- // ------- Header row(s) -------
693
- if (!tuplesOnly) {
694
- // For wrapped mode the header column may itself need wrapping if its
695
- // own width exceeds the (possibly shrunk) column width. Upstream
696
- // `print_aligned_text` only computes header line breaks based on `\n`
697
- // splits — header text that's wider than the column wraps onto the
698
- // next display line via the same wrap loop as the data path. We
699
- // mirror that by passing the header text through `wrapLine` in
700
- // wrapped mode (and through the simple `\n` split for plain mode).
701
- const headerColLines = headerCells.map((c, i) => {
702
- if (!wrapped)
703
- return c.lines;
704
- const out = [];
705
- for (const line of c.lines) {
706
- out.push(...wrapLine(line, widths[i]));
707
- }
708
- return out;
709
- });
710
- const headerLineCount = Math.max(1, ...headerColLines.map((l) => l.length));
711
- for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
712
- // Per-column wrap state for this physical header line:
713
- // `wrap` the cell has more content to emit on the next line
714
- // via in-cell wrapping (wrap_left/wrap_right marker)
715
- // `nl` the cell has more `\n`-split content below
716
- // (header_nl_left/header_nl_right marker)
717
- // `none` the cell is done (already emitted its last line)
718
- const cellWrapPrev = headerColLines.map((l) => (lineIdx > 0 && lineIdx < l.length ? "nl" : "none"));
719
- const cellWrapNext = headerColLines.map((l) => (lineIdx < l.length - 1 ? "nl" : "none"));
720
- out += renderHeaderLine(headerColLines.map((l) => l[lineIdx] ?? ""), widths, border, glyphs, cellWrapPrev, cellWrapNext, lineIdx === 0);
721
- out += newline;
722
- }
723
- // Header rule.
724
- out += buildRule(widths, border, glyphs, "middle") + newline;
725
- }
726
- // ------- Data rows -------
727
- for (let r = 0; r < cellGrid.length; r++) {
728
- const row = cellGrid[r];
729
- // For wrapped mode, each cell may need to be re-broken to fit
730
- // the (possibly shrunk) column width.
731
- const colLines = row.map((cell, i) => {
732
- const lines = cell.lines;
733
- if (!wrapped)
734
- return lines;
735
- // Each source line may itself need breaking down to fit.
736
- const out = [];
737
- for (const line of lines) {
738
- out.push(...wrapLine(line, widths[i]));
739
- }
740
- return out;
741
- });
742
- // We also need to know, per column, which output lines originated as
743
- // wrap continuations (vs. `\n` splits) so the markers can pick the
744
- // correct glyph: `.` (wrap_left/right) when wrapped from an
745
- // over-width source line, `+` (nl_left/right) when split by a literal
746
- // newline in the cell content.
747
- //
748
- // We classify by walking the original cell.lines: each source line
749
- // expands to N display lines (N >= 1) via `wrapLine`. The first
750
- // expansion is "nl" (or the very first line of the cell, which has
751
- // no marker), the rest are "wrap" continuations of the same source
752
- // line.
753
- const colLineKinds = row.map((cell, i) => {
754
- const kinds = [];
755
- const lines = cell.lines;
756
- for (let si = 0; si < lines.length; si++) {
757
- const chunks = wrapped
758
- ? wrapLine(lines[si], widths[i])
759
- : [lines[si]];
760
- for (let ci = 0; ci < chunks.length; ci++) {
761
- if (si === 0 && ci === 0)
762
- kinds.push("start");
763
- else if (ci === 0)
764
- kinds.push("nl");
765
- else
766
- kinds.push("wrap");
767
- }
768
- }
769
- return kinds;
770
- });
771
- const lineCount = Math.max(1, ...colLines.map((l) => l.length));
772
- for (let li = 0; li < lineCount; li++) {
773
- // For each column, determine the wrap state for THIS output line.
774
- // The LEFT marker is selected by `cellWrapPrev[j]`: what kind of
775
- // continuation made this line exist (the `kind` of the current
776
- // line — `wrap` or `nl` — only matters when li > 0).
777
- // The RIGHT marker is selected by `cellWrapNext[j]`: the kind of
778
- // the NEXT line (li + 1), which tells us whether this cell is
779
- // still emitting more content.
780
- const cellWrapPrev = colLineKinds.map((kinds, j) => {
781
- if (li === 0 || li >= colLines[j].length)
782
- return "none";
783
- return kinds[li] === "wrap" ? "wrap" : "nl";
784
- });
785
- const cellWrapNext = colLineKinds.map((kinds, j) => {
786
- const nextIdx = li + 1;
787
- if (nextIdx >= colLines[j].length)
788
- return "none";
789
- return kinds[nextIdx] === "wrap" ? "wrap" : "nl";
790
- });
791
- out += renderDataLine(colLines.map((l) => l[li] ?? ""), widths, aligns, border, glyphs, cellWrapPrev, cellWrapNext, li === 0);
792
- out += newline;
793
- }
794
- // Heavy borders (border == 3) insert a rule between rows.
795
- if (border === 3 && r < cellGrid.length - 1) {
796
- out += buildRule(widths, border, glyphs, "middle") + newline;
797
- }
798
- }
799
- // Bottom rule when border 2/3.
800
- if (!tuplesOnly && (border === 2 || border === 3)) {
801
- out += buildRule(widths, border, glyphs, "bottom") + newline;
802
- }
803
- // Footer: (N rows) + trailing blank line. Upstream's
804
- // `print_aligned_text` emits `printTableAddFooter()` and then
805
- // `printTableCleanup()` adds a separator `\n`. Verified against
806
- // vanilla psql 18: `SELECT 1; SELECT 2;` emits each result followed
807
- // by `(1 row)\n\n` so consecutive queries are visually separated.
808
- //
809
- // Footer handling mirrors `footers_with_default` (print.c lines
810
- // 397-413): user footers SUPPRESS the default `(N rows)` row counter.
811
- // The default is only emitted when there are no user footers AND
812
- // `defaultFooter` is true. This matters for `\d` output, which
813
- // attaches user footers ("Access method: ...", "Owned by: ...") and
814
- // doesn't want the row counter to interleave between the data rows
815
- // and the footers.
816
- const hasUserFooters = !!opts.footers && opts.footers.length > 0;
817
- if (!tuplesOnly && topt.defaultFooter && !hasUserFooters) {
818
- const n = rs.rows.length;
819
- out += `(${String(n)} ${n === 1 ? "row" : "rows"})` + newline;
820
- }
821
- if (!tuplesOnly && opts.footers) {
822
- for (const f of opts.footers)
823
- out += f + newline;
824
- }
825
- // Trailing blank line between query results. Upstream `print.c`
826
- // line 1196 emits `fputc('\n', fout)` ONLY when `stop_table` is set —
827
- // chunked-cursor mid-error printing leaves it unset so the ERROR line
828
- // lands flush against the last data row. Default is `true`, so happy-path
829
- // queries still get the blank separator before the next command.
830
- if (topt.stopTable) {
831
- out += newline;
832
- }
833
- return out;
834
- };
835
- /**
836
- * Pick the column-separator glyph between two adjacent cells. ASCII and
837
- * Unicode collapse `midvruleNl` / `midvruleWrap` / `midvruleBlank` onto the
838
- * base `vrule`, so the alt-glyph branches only matter for `old-ascii`,
839
- * which uses `:` for `\n` continuations, `;` for wrap continuations, and
840
- * `" "` when the joining column is exhausted but the line still exists
841
- * because of a wrap/continuation elsewhere in the row.
842
- *
843
- * Upstream picks the midvrule from the column *to the right of the
844
- * separator* (`right`) — if that column is past-end, the separator
845
- * collapses to `midvruleBlank` regardless of the left column's state.
846
- *
847
- * `firstLine = true` short-circuits to the base `vrule` — upstream
848
- * only consults the midvrule table once at least one continuation has
849
- * been emitted, so the first display line of every row always uses
850
- * the regular vrule even if a column will wrap on the next line.
851
- */
852
- const pickMidvrule = (glyphs,
853
- // Left col's state at this line (cellWrapPrev[i]) — unused by the
854
- // upstream rule but kept in the signature for clarity at call sites.
855
- _left, right, firstLine) => {
856
- if (firstLine)
857
- return glyphs.vrule;
858
- if (right === "wrap")
859
- return glyphs.midvruleWrap;
860
- if (right === "nl")
861
- return glyphs.midvruleNl;
862
- return glyphs.midvruleBlank;
863
- };
864
- /**
865
- * Right-marker glyph for a data row's cell. Mirrors the `wrap[j]`-driven
866
- * fputs at upstream `print.c` lines 1151-1156:
867
- * PRINT_LINE_WRAP_WRAP → format->wrap_right (`.` ascii, `…` unicode)
868
- * PRINT_LINE_WRAP_NEWLINE → format->nl_right (`+` ascii, `↵` unicode)
869
- * PRINT_LINE_WRAP_NONE → " " (only when not at the trailing edge
870
- * without border)
871
- */
872
- const dataRightMarker = (state, glyphs, isLast, border) => {
873
- if (state === "wrap")
874
- return glyphs.wrapRight;
875
- if (state === "nl")
876
- return glyphs.nlRight;
877
- // state === 'none': trailing edge — only emit a space when there's
878
- // something AFTER us (another column to follow, or the right border
879
- // of a border=2/3 box). For the LAST cell on a borderless row, nothing.
880
- if (isLast && border !== 2 && border !== 3)
881
- return "";
882
- return " ";
883
- };
884
- /**
885
- * Left-marker glyph for a data row's cell. Mirrors `wrap[j]` at upstream
886
- * `print.c` lines 1066-1073:
887
- * PRINT_LINE_WRAP_WRAP → format->wrap_left (`.` ascii, `…` unicode)
888
- * PRINT_LINE_WRAP_NEWLINE → format->nl_left (" " for both ascii/unicode)
889
- * PRINT_LINE_WRAP_NONE → " "
890
- *
891
- * Only emitted when border != 0 (the leading-gutter slot). For border=0
892
- * upstream skips this entirely (no leading marker).
893
- */
894
- const dataLeftMarker = (state, glyphs) => {
895
- if (state === "wrap")
896
- return glyphs.wrapLeft;
897
- if (state === "nl")
898
- return glyphs.nlLeft;
899
- return " ";
900
- };
901
- /**
902
- * Render one display-line of a header row. Centered cells, fixed
903
- * `header_nl_*` markers on continuation lines.
904
- *
905
- * Layout (with ASCII glyphs):
906
- * border 0: `<c1centered>[H]<c2centered>[H]` — `wrap_right_border=true`
907
- * means the trailing slot still gets emitted even at table
908
- * edge. `H` = `header_nl_right` ("+") when the next line in
909
- * this column has more content, else " ".
910
- * border 1: ` <c1centered>[H]| <c2centered>[H]`
911
- * (leading gutter, header_nl_right marker between content and
912
- * separator, optional trailing " " on the very last cell)
913
- * border 2: `| <c1centered>[H]| <c2centered>[H]|` (full box)
914
- */
915
- const renderHeaderLine = (cells, widths, border, glyphs,
916
- // What kind of continuation produced THIS line (curr_nl_line > 0 in
917
- // upstream): determines the leading marker on this side of the cell.
918
- // Per-col `cellWrapPrev[i]` only drives the trailing marker now; the
919
- // leading-gutter decision uses `firstLine` (curr_nl_line == 0).
920
- cellWrapPrev,
921
- // What kind of continuation follows on the NEXT line: determines
922
- // the trailing marker for this cell.
923
- cellWrapNext,
924
- // True on the very first display line of the header (curr_nl_line == 0
925
- // in upstream). Upstream emits `header_nl_left` for ALL non-first cells
926
- // whenever curr_nl_line > 0 — even for cells that are themselves
927
- // exhausted on that line — so the choice can't be made per-column.
928
- firstLine) => {
929
- const { vrule } = glyphs;
930
- let out = "";
931
- if (border === 2 || border === 3)
932
- out += vrule;
933
- for (let i = 0; i < cells.length; i++) {
934
- const isLast = i === cells.length - 1;
935
- // Leading gutter / wrap marker. Upstream `print.c` line 981-984:
936
- // if (opt_border != 0 || (!format->wrap_right_border && i > 0))
937
- // fputs(curr_nl_line ? format->header_nl_left : " ", fout);
938
- // For ASCII (wrap_right_border=true): emitted iff border != 0.
939
- // For old-ascii (wrap_right_border=false): emitted for non-first
940
- // cells too at border=0 — the inter-column slot doubles as the
941
- // leading-gutter slot of col[i].
942
- if (border !== 0 || (!glyphs.wrapRightBorder && i > 0)) {
943
- out += firstLine ? " " : glyphs.headerNlLeft;
944
- }
945
- // Header cells are always centered and padded to full width
946
- // (upstream emits `%-*s%s%-*s` regardless of position — no
947
- // skip-padding-on-last-cell quirk on header rows).
948
- out += padToWidth(cells[i], widths[i], "center");
949
- // Trailing marker. Upstream `print.c` line 1003-1005:
950
- // if (opt_border != 0 || format->wrap_right_border)
951
- // fputs(!header_done[i] ? format->header_nl_right : " ", fout);
952
- // For ASCII this is always emitted (wrap_right_border=true), so
953
- // multi-line headers get `+` between content and the column
954
- // separator AND on the trailing edge of the last column. For
955
- // OLD-ASCII (wrap_right_border=false) this slot is skipped at
956
- // border=0 — the inter-column space comes from the NEXT cell's
957
- // leading-gutter emit on the next loop iteration.
958
- if (border !== 0 || glyphs.wrapRightBorder) {
959
- out += cellWrapNext[i] !== "none" ? glyphs.headerNlRight : " ";
960
- }
961
- // Column divider (not on the last column). For border 0 ASCII the
962
- // trailing-marker slot above already consumes the inter-column gap.
963
- // For border 0 OLD-ASCII the next iteration emits the leading-gutter
964
- // slot, so no extra space is needed here either.
965
- if (!isLast && border !== 0) {
966
- out += vrule;
967
- }
968
- }
969
- if (border === 2 || border === 3)
970
- out += vrule;
971
- return out;
972
- };
973
- /**
974
- * Render one display-line of a data row.
975
- *
976
- * Mirrors upstream `print_aligned_text` (print.c lines 1047-1180): one
977
- * loop body, per-column emit `[left-marker] <content> [right-marker]
978
- * [column-divider]`, then `[right-border]`. Padding is applied when:
979
- * - the cell is not the last column (finalspaces=true in upstream),
980
- * - OR `wrap[j]` is set (cell continues, so marker needs an aligned
981
- * position).
982
- *
983
- * Right-aligned columns always pad on the left (spaces first), but per
984
- * upstream they get a trailing right-margin pad only at border=2 (the
985
- * `finalspaces` branch). For border 0/1 right-aligned columns we follow
986
- * the same rule: padding before content, no trailing pad on the last
987
- * column when there's no continuation.
988
- */
989
- const renderDataLine = (cells, widths, aligns, border, glyphs,
990
- // For each column, the "wrap state" carried over from the previous
991
- // physical line (drives the LEFT marker — `wrap[j]` in upstream's
992
- // pre-content fputs).
993
- cellWrapPrev,
994
- // For each column, the wrap state determined by THIS line's content
995
- // and whatever follows on the NEXT line (drives the RIGHT marker and
996
- // the column divider glyph).
997
- cellWrapNext,
998
- // True only on the first display line of a row. Used by old-ascii to
999
- // keep the regular `|` separator on the row's first line (the alt
1000
- // midvrules only kick in once at least one continuation has been
1001
- // emitted).
1002
- firstLine) => {
1003
- const { vrule } = glyphs;
1004
- let out = "";
1005
- if (border === 2 || border === 3)
1006
- out += vrule;
1007
- for (let i = 0; i < cells.length; i++) {
1008
- const isLast = i === cells.length - 1;
1009
- // Left marker. Border 0 has no leading-gutter slot for non-first
1010
- // cells under ASCII (wrap_right_border=true); under OLD-ASCII the
1011
- // leading-gutter slot doubles as the inter-column space for i > 0
1012
- // (upstream `print.c` line 1066-1073).
1013
- if (border !== 0) {
1014
- out += dataLeftMarker(cellWrapPrev[i], glyphs);
1015
- }
1016
- else if (!glyphs.wrapRightBorder && i > 0) {
1017
- out += dataLeftMarker(cellWrapPrev[i], glyphs);
1018
- }
1019
- // Decide whether to pad this cell. Upstream `print.c` lines 1141-1148:
1020
- // left-aligned cells pad iff `finalspaces || wrap[j] != NONE`.
1021
- // right-aligned cells always pad on the left side regardless.
1022
- // `finalspaces` = (border == 2 || not last column).
1023
- const finalspaces = border === 2 || border === 3 || !isLast;
1024
- const needsTrailingPad = finalspaces || cellWrapNext[i] !== "none";
1025
- if (aligns[i] === "right") {
1026
- // Right-aligned cells always get full padding on the left so
1027
- // content right-aligns. Trailing pad only when finalspaces (or
1028
- // wrap, to keep marker aligned).
1029
- out += padToWidth(cells[i], widths[i], "right");
1030
- }
1031
- else if (needsTrailingPad) {
1032
- out += padToWidth(cells[i], widths[i], "left");
1033
- }
1034
- else {
1035
- // Last cell, no wrap state — emit raw content (no trailing pad).
1036
- out += cells[i];
1037
- }
1038
- // Right marker. Upstream `print.c` lines 1150-1156:
1039
- // - WRAP → wrap_right
1040
- // - NEWLINE→ nl_right
1041
- // - NONE → space (only when not last col / border=2)
1042
- // For OLD-ASCII at border 0 the inter-column trailing slot of
1043
- // non-last cells is owned by the *next* iteration's leading-gutter
1044
- // emit (single space either way). For the very last column, upstream
1045
- // still emits a trailing char when the cell is on a continuation
1046
- // (`cellWrapNext != none`) so wrapped cells keep their trailing
1047
- // column position aligned with the first line.
1048
- if (border !== 0 || glyphs.wrapRightBorder) {
1049
- out += dataRightMarker(cellWrapNext[i], glyphs, isLast, border);
1050
- }
1051
- else if (isLast && cellWrapNext[i] !== "none") {
1052
- out += dataRightMarker(cellWrapNext[i], glyphs, isLast, border);
1053
- }
1054
- // Column divider. Upstream lines 1158-1169: in old-ascii the
1055
- // midvrule between col[i] and col[i+1] swaps to a continuation
1056
- // glyph when *either* adjacent cell is on a wrap/nl continuation
1057
- // (midvrule_nl=":", midvrule_wrap=";", midvrule_blank=" "). For
1058
- // ASCII / Unicode all three midvrule_* equal the regular vrule, so
1059
- // the branch collapses to `vrule`.
1060
- if (!isLast && border !== 0) {
1061
- out += pickMidvrule(glyphs, cellWrapPrev[i], cellWrapPrev[i + 1], firstLine);
1062
- }
1063
- }
1064
- if (border === 2 || border === 3)
1065
- out += vrule;
1066
- return out;
1067
- };
1068
- /**
1069
- * Break a single line of text into chunks of at most `width` visible
1070
- * columns. Greedy, code-point-aware. Used by wrapped mode and by
1071
- * vertical mode for value wrapping.
1072
- */
1073
- const wrapLine = (line, width) => {
1074
- if (width <= 0)
1075
- return [line];
1076
- if (displayWidth(line) <= width)
1077
- return [line];
1078
- const chunks = [];
1079
- let buf = "";
1080
- let bufW = 0;
1081
- for (const ch of line) {
1082
- const cw = codePointWidth(ch.codePointAt(0) ?? 0);
1083
- if (bufW + cw > width && buf.length > 0) {
1084
- chunks.push(buf);
1085
- buf = "";
1086
- bufW = 0;
1087
- }
1088
- buf += ch;
1089
- bufW += cw;
1090
- }
1091
- if (buf.length > 0)
1092
- chunks.push(buf);
1093
- return chunks;
1094
- };
1095
- // ---------------------------------------------------------------------------
1096
- // Vertical / expanded rendering.
1097
- // ---------------------------------------------------------------------------
1098
- const renderVertical = (rs, opts) => {
1099
- const topt = opts.topt;
1100
- const border = topt.border;
1101
- const tuplesOnly = topt.tuplesOnly;
1102
- const nullPrint = opts.nullPrint !== "" ? opts.nullPrint : topt.nullPrint;
1103
- const glyphs = glyphsFor(topt.unicodeBorderLineStyle);
1104
- // `old-ascii` swaps several emission decisions (leading-slot at border<2,
1105
- // suppressed data trailing marker, alternate midvrule glyph on
1106
- // continuation lines). Track once at the top so the per-line code stays
1107
- // readable.
1108
- const oldAscii = topt.unicodeBorderLineStyle === "old-ascii";
1109
- const headers = rs.fields.map((f) => f.name);
1110
- // Width of the name column = max header line width (multi-line headers
1111
- // count per-line; upstream `pg_wcssize` returns the widest line).
1112
- let nameWidth = 0;
1113
- let hmultiline = false;
1114
- for (const h of headers) {
1115
- for (const line of h.split("\n")) {
1116
- const w = displayWidth(line);
1117
- if (w > nameWidth)
1118
- nameWidth = w;
1119
- }
1120
- if (h.includes("\n"))
1121
- hmultiline = true;
1122
- }
1123
- // Width of the value column = max value width across all rows.
1124
- let valueWidth = 0;
1125
- let dmultiline = false;
1126
- const boolDisplay = boolDisplayOf(topt);
1127
- const cellGrid = rs.rows.map((row) => row.map((cell, i) => renderCell(cell, nullPrint, topt.numericLocale, rs.fields[i]?.dataTypeID, boolDisplay)));
1128
- for (const row of cellGrid) {
1129
- for (const v of row) {
1130
- if (v.includes("\n"))
1131
- dmultiline = true;
1132
- for (const line of v.split("\n")) {
1133
- const w = displayWidth(line);
1134
- if (w > valueWidth)
1135
- valueWidth = w;
1136
- }
1137
- }
1138
- }
1139
- // Compute dwidth — the value-column width used for both the record
1140
- // header padding and the per-cell value wrap. Mirrors upstream
1141
- // `print_aligned_vertical` in `fe_utils/print.c` lines 1463-1583: the
1142
- // value column shrinks to fit `output_columns` (with a hard floor
1143
- // enforced via `min_width` and `rwidth`) when `\pset format wrapped`
1144
- // AND `\pset columns N` is in effect. Runs for all borders 0/1/2.
1145
- //
1146
- // The `swidth` table below mirrors the upstream branches:
1147
- // border 0: 1 (gutter) +1 if hmultiline
1148
- // +1 if dmultiline (border<2 && !oldAscii)
1149
- // border 1: 3 (` | `) +1 if hmultiline && oldAscii (left newline marker)
1150
- // +1 if dmultiline (border<2 && !oldAscii)
1151
- // border 2: 7 (outer vrules + spacers; no dmultiline bump)
1152
- //
1153
- // `rwidth` is the natural label width (`* RECORD N` + digit count).
1154
- // The two-pass loop turns `dmultiline` on the first iteration if a
1155
- // wrap is needed, then recomputes with the bumped swidth. We
1156
- // implement that as a single pass over the two possible swidths.
1157
- const wrappedMode = topt.format === "wrapped";
1158
- let dwidth = valueWidth;
1159
- if (wrappedMode) {
1160
- const outputColumns = topt.columns > 0 ? topt.columns : topt.envColumns;
1161
- if (outputColumns > 0) {
1162
- let baseSwidth;
1163
- if (border === 0)
1164
- baseSwidth = 1 + (hmultiline ? 1 : 0);
1165
- else if (border === 1)
1166
- baseSwidth = 3 + (hmultiline && oldAscii ? 1 : 0);
1167
- else
1168
- baseSwidth = 7;
1169
- // dmultiline adds a marker column only when border < 2 and not old-ascii
1170
- // (old-ascii suppresses the data trailing marker entirely at border<2).
1171
- const dmAddOk = border < 2 && !oldAscii;
1172
- const swidthInit = baseSwidth + (dmAddOk && dmultiline ? 1 : 0);
1173
- const swidthAfterWrap = baseSwidth + (dmAddOk ? 1 : 0);
1174
- // rwidth = label-width floor: `* RECORD N`(9), `-[ RECORD ]`(12),
1175
- // or `+-[ RECORD ]-+`(15), each plus digit count for N.
1176
- const labelOverhead = border === 0 ? 9 : border === 1 ? 12 : 15;
1177
- const nrows = cellGrid.length;
1178
- const rwidth = labelOverhead +
1179
- (nrows > 0
1180
- ? 1 + Math.floor(Math.log10(Math.max(1, nrows)))
1181
- : 0);
1182
- const compute = (swidth) => {
1183
- let width = nameWidth + swidth + valueWidth;
1184
- if (width < rwidth)
1185
- width = rwidth;
1186
- let minWidth = nameWidth + swidth + 3;
1187
- if (minWidth < rwidth)
1188
- minWidth = rwidth;
1189
- if (outputColumns >= width)
1190
- return width - nameWidth - swidth;
1191
- if (outputColumns < minWidth)
1192
- return minWidth - nameWidth - swidth;
1193
- return outputColumns - nameWidth - swidth;
1194
- };
1195
- // First pass with the natural swidth.
1196
- let newDwidth = compute(swidthInit);
1197
- // If wrap is needed (newDwidth < natural) AND dmultiline wasn't
1198
- // already true AND we're at border<2, upstream toggles dmultiline
1199
- // on and re-runs with swidth+1.
1200
- if (newDwidth < valueWidth && !dmultiline && dmAddOk) {
1201
- newDwidth = compute(swidthAfterWrap);
1202
- dmultiline = true;
1203
- }
1204
- // Clamp: never grow the data column beyond the natural value
1205
- // width (matches `dwidth = newdwidth` in upstream).
1206
- dwidth = newDwidth < valueWidth ? newDwidth : valueWidth;
1207
- }
1208
- }
1209
- let out = "";
1210
- const newline = "\n";
1211
- if (!tuplesOnly && topt.title) {
1212
- out += topt.title + newline;
1213
- }
1214
- if (rs.rows.length === 0) {
1215
- // Expanded mode on empty result: upstream emits the "(0 rows)"
1216
- // footer (same wording as horizontal mode) followed by the trailing
1217
- // blank-line separator, NOT a special "(No rows)" string. Verified
1218
- // against vanilla psql 18: `\\pset expanded on` then `select 1
1219
- // where false;` emits `(0 rows)\n\n`.
1220
- if (!tuplesOnly)
1221
- out += "(0 rows)" + newline;
1222
- if (!tuplesOnly && opts.footers) {
1223
- for (const f of opts.footers)
1224
- out += f + newline;
1225
- }
1226
- if (!tuplesOnly)
1227
- out += newline;
1228
- return out;
1229
- }
1230
- // Decide whether multi-line markers should be emitted on header/data
1231
- // continuations. Mirrors upstream `print_aligned_vertical` predicates
1232
- // at print.c lines 1679-1689 (header right marker) and 1747-1763 (data
1233
- // trailing marker):
1234
- // non-old-ascii: header marker iff `border > 0 || hmultiline`
1235
- // data marker iff `border > 1 || dmultiline`
1236
- // old-ascii: header right marker emitted iff `border > 0`
1237
- // data right marker emitted iff `border > 1`
1238
- // (old-ascii relies on header_nl_LEFT for header
1239
- // continuations and never emits the data right marker
1240
- // at border<2 — it uses the alt midvrule glyph instead.)
1241
- const emitHeaderMarker = oldAscii ? border > 0 : border > 0 || hmultiline;
1242
- const emitDataMarker = oldAscii ? border > 1 : border > 1 || dmultiline;
1243
- // Leading-slot for header column. Upstream `print_aligned_vertical`
1244
- // line 1655-1657 emits the slot at `border==2 || (hmultiline &&
1245
- // oldAscii)`. Old-ascii routes the continuation marker through the
1246
- // leading gutter (`+` in `headerNlLeft`) instead of the trailing slot.
1247
- const emitHeaderLeftSlot = border > 1 || (hmultiline && oldAscii);
1248
- // Record-header label width. Upstream line 1610-1613 bumps `lhwidth`
1249
- // by 1 at border<2 with hmultiline && oldAscii — the extra column is
1250
- // the same `+` newline-indicator slot the data lines reserve.
1251
- const lhwidth = border < 2 && hmultiline && oldAscii ? nameWidth + 1 : nameWidth;
1252
- for (let r = 0; r < cellGrid.length; r++) {
1253
- if (!tuplesOnly) {
1254
- out += renderRecordHeader(r + 1, lhwidth, dwidth, border, glyphs, r === 0);
1255
- out += newline;
1256
- }
1257
- else if (r > 0 || border === 2 || border === 3) {
1258
- // tuples_only: upstream `print_aligned_vertical` line 1619-1621
1259
- // still emits the inter-record separator (with the `* Record N`
1260
- // label suppressed) for r>0, and the top rule at border=2/3 for
1261
- // the very first record. Reuses the same record-header path with
1262
- // `record=0` to suppress the label glyph.
1263
- out += renderRecordHeader(0, lhwidth, dwidth, border, glyphs, r === 0);
1264
- out += newline;
1265
- }
1266
- for (let c = 0; c < headers.length; c++) {
1267
- const headerLines = headers[c].split("\n");
1268
- const dataLines = cellGrid[r][c].split("\n");
1269
- out += renderVerticalCell(headerLines, dataLines, nameWidth, dwidth, border, glyphs, hmultiline, emitHeaderMarker, emitDataMarker, emitHeaderLeftSlot, oldAscii);
1270
- }
1271
- }
1272
- if (!tuplesOnly && (border === 2 || border === 3)) {
1273
- out +=
1274
- glyphs.botLeft +
1275
- glyphs.hrule.repeat(nameWidth + 2) +
1276
- glyphs.botMid +
1277
- glyphs.hrule.repeat(dwidth + 2) +
1278
- glyphs.botRight +
1279
- newline;
1280
- }
1281
- // Footer + trailing blank-line handling mirrors upstream
1282
- // `print_aligned_vertical` (print.c lines 1804-1822):
1283
- // - At border < 2, emit a blank line BEFORE the user footers
1284
- // (separates the last data line from the footer block).
1285
- // - At border >= 2, the bottom rule already provides the separator
1286
- // so no extra blank is emitted before footers.
1287
- // - Always emit a trailing blank line at the very end —
1288
- // `fputc('\n', fout)` on line 1821 is UNCONDITIONAL (not gated by
1289
- // `opt_tuples_only`), so tuples_only mode still gets a single
1290
- // separator before the next command.
1291
- // - Expanded mode never emits a `(N rows)` default footer; the
1292
- // trailing blank is the only output between consecutive results.
1293
- const hasUserFooters = !!opts.footers && opts.footers.length > 0;
1294
- if (!tuplesOnly && hasUserFooters && border < 2) {
1295
- out += newline;
1296
- }
1297
- if (!tuplesOnly && opts.footers) {
1298
- for (const f of opts.footers)
1299
- out += f + newline;
1300
- }
1301
- out += newline;
1302
- return out;
1303
- };
1304
- /**
1305
- * Render the `[ RECORD N ]` block header for vertical mode.
1306
- *
1307
- * border 0: `* Record N<spaces-to-fill>` (padded to `nameWidth + valueWidth`)
1308
- * border 1: `-[ RECORD N ]<dashes-to-fit>+<dashes-to-fit>`
1309
- * border 2: `+-[ RECORD N ]<dashes>+<dashes>+`
1310
- *
1311
- * Mirrors psql `print_aligned_vertical_line`.
1312
- *
1313
- * Layout invariants (verified empirically against vanilla psql 18):
1314
- *
1315
- * border 1 — data line is `<name padded to nameWidth> | <value>`:
1316
- * - The `|` column-separator sits at position `nameWidth + 2`.
1317
- * - Record header `+` mid junction aligns with that `|`, so the
1318
- * pre-junction (left) span is `nameWidth + 1` chars and the
1319
- * post-junction (right) span is `valueWidth + 1` chars.
1320
- * - When `1 + label.length` overflows the pre-junction span but the
1321
- * label still fits in the full data row width, upstream pads with
1322
- * hrule chars to the row width and OMITS the mid junction
1323
- * (`-[ RECORD 1 ]--` for `nameWidth=2, valueWidth=10`).
1324
- * - When `1 + label.length` overflows the row width too, upstream
1325
- * emits just the label-prefix with no padding (the label IS the
1326
- * whole divider line).
1327
- *
1328
- * border 2 — data line is `| <name> | <value> |`, row width is
1329
- * `1 + (nameWidth + 2) + 1 + (valueWidth + 2) + 1`:
1330
- * - Outer corners use the TOP glyph set on the first record
1331
- * (`topLeft`, `topMid`, `topRight`) and the MID glyph set on
1332
- * subsequent records (`midLeft`, `midMid`, `midRight`). The bottom
1333
- * rule (emitted by `renderVertical`) uses the BOT glyphs.
1334
- * - The `topMid` (or `midMid`) junction sits at position
1335
- * `leftSegLen + 2` to match the `|` column-separator in the data
1336
- * row. When `1 + label.length` overflows the left segment, the
1337
- * junction glyph is dropped and the row is padded entirely with
1338
- * hrule chars between the outer corners.
1339
- *
1340
- * border 0 — label is padded with spaces to `nameWidth + valueWidth`.
1341
- * Labels that exceed `nameWidth + valueWidth` pass through unchanged.
1342
- *
1343
- * `isFirst` only affects border 2 glyph selection; border 0 and 1 share
1344
- * a single set of single-line rule glyphs across all records.
1345
- */
1346
- const renderRecordHeader = (record, nameWidth, valueWidth, border, glyphs, isFirst) => {
1347
- const { hrule } = glyphs;
1348
- // `record === 0` is the tuples_only "separator-only" sentinel — upstream
1349
- // `print_aligned_vertical_line` (print.c line 1244-1249) gates the
1350
- // label emission on `if (record)`, so passing 0 yields a bare rule
1351
- // with no `* Record N` / `[ RECORD N ]` text. At border=0 that's a
1352
- // blank line padded to width; at border>=1 a continuous hrule run.
1353
- if (record === 0) {
1354
- if (border === 0) {
1355
- return " ".repeat(nameWidth + valueWidth);
1356
- }
1357
- if (border === 1) {
1358
- const rowWidth = nameWidth + 3 + valueWidth;
1359
- const leftSpan = nameWidth + 1;
1360
- // Mid junction lands at the `|` position in data rows.
1361
- return (hrule.repeat(leftSpan) +
1362
- glyphs.midMid +
1363
- hrule.repeat(rowWidth - leftSpan - 1));
1364
- }
1365
- // border 2/3.
1366
- const outerLeft = isFirst ? glyphs.topLeft : glyphs.midLeft;
1367
- const outerRight = isFirst ? glyphs.topRight : glyphs.midRight;
1368
- const outerMid = isFirst ? glyphs.topMid : glyphs.midMid;
1369
- return (outerLeft +
1370
- hrule.repeat(nameWidth + 2) +
1371
- outerMid +
1372
- hrule.repeat(valueWidth + 2) +
1373
- outerRight);
1374
- }
1375
- if (border === 0) {
1376
- const label = `* Record ${String(record)}`;
1377
- const target = nameWidth + valueWidth;
1378
- if (label.length >= target)
1379
- return label;
1380
- return label + " ".repeat(target - label.length);
1381
- }
1382
- const label = `[ RECORD ${String(record)} ]`;
1383
- // border 1: `-[ RECORD N ]---+---------`
1384
- if (border === 1) {
1385
- // Data row width = nameWidth + 3 + valueWidth (`<name padded> | <value>`).
1386
- // Pre-junction span = nameWidth + 1 (name col + gutter space). The
1387
- // `+` mid junction lands at position `nameWidth + 2`, mirroring the
1388
- // `|` in the data lines. Post-junction span = valueWidth + 1.
1389
- const rowWidth = nameWidth + 3 + valueWidth;
1390
- const leftSpan = nameWidth + 1;
1391
- const rightSpan = valueWidth + 1;
1392
- const left = hrule + label;
1393
- if (left.length <= leftSpan) {
1394
- // Label fits in the pre-junction span. Pad left to leftSpan, emit
1395
- // the mid junction (`+`), then rightSpan hrules.
1396
- const leftPadded = left + hrule.repeat(leftSpan - left.length);
1397
- return leftPadded + glyphs.midMid + hrule.repeat(rightSpan);
1398
- }
1399
- if (left.length < rowWidth) {
1400
- // Label overflows the pre-junction span but still fits within the
1401
- // full row. Pad the label-prefix out to the row width with hrule
1402
- // chars and DROP the mid junction (vanilla psql parity).
1403
- return left + hrule.repeat(rowWidth - left.length);
1404
- }
1405
- // Label exceeds the full row width — emit just the label-prefix
1406
- // (no padding, no junction).
1407
- return left;
1408
- }
1409
- // border 2 / 3.
1410
- //
1411
- // Outer corners come from the TOP glyph set on the first record (so
1412
- // the leading rule looks like a normal table top) and from the MID
1413
- // glyph set on subsequent records (so it looks like an inter-row rule).
1414
- // For ASCII these all collapse to `+`; the distinction matters for
1415
- // Unicode (`┌`/`┐`/`┬` vs `├`/`┤`/`┼`).
1416
- const outerLeft = isFirst ? glyphs.topLeft : glyphs.midLeft;
1417
- const outerRight = isFirst ? glyphs.topRight : glyphs.midRight;
1418
- const outerMid = isFirst ? glyphs.topMid : glyphs.midMid;
1419
- // Data row width = 1 + (nameWidth + 2) + 1 + (valueWidth + 2) + 1.
1420
- const leftSegLen = nameWidth + 2;
1421
- const rightSegLen = valueWidth + 2;
1422
- const leftCore = hrule + label;
1423
- if (leftCore.length <= leftSegLen) {
1424
- // Label fits in the left segment. Pad left to leftSegLen, emit the
1425
- // top/mid junction, then rightSegLen hrules, then top/mid right
1426
- // corner.
1427
- const leftPadded = leftCore + hrule.repeat(leftSegLen - leftCore.length);
1428
- return (outerLeft +
1429
- leftPadded +
1430
- outerMid +
1431
- hrule.repeat(rightSegLen) +
1432
- outerRight);
1433
- }
1434
- // Label overflows the left segment. Drop the mid junction and pad
1435
- // hrules between the outer corners. When the natural row inner width
1436
- // (= leftSegLen + 1 + rightSegLen) is itself smaller than the label
1437
- // plus a single trailing hrule, the rule grows past the data-row
1438
- // width so the label always has at least one `─`/`-` of breathing
1439
- // room before the closing corner (verified against vanilla psql 18:
1440
- // `\gx` on `SELECT 1 as one, 2 as two \pset border 2` emits
1441
- // `┌─[ RECORD 1 ]─┐` — 14 inner chars vs the 9-char inner data row).
1442
- const innerWidth = Math.max(leftSegLen + 1 + rightSegLen, leftCore.length + 1);
1443
- return (outerLeft +
1444
- leftCore +
1445
- hrule.repeat(innerWidth - leftCore.length) +
1446
- outerRight);
1447
- };
1448
- /**
1449
- * Render one cell (header + value) of an expanded-mode record.
1450
- *
1451
- * Mirrors the per-cell loop in upstream `print_aligned_vertical`
1452
- * (print.c lines 1636-1801). Each iteration of the outer loop emits ONE
1453
- * physical output line containing both the header part and the data
1454
- * part. Iteration continues until BOTH the header and data sides are
1455
- * exhausted (so a 3-line header against a 1-line value still emits 3
1456
- * lines, with the data side blank from line 2 onward).
1457
- *
1458
- * Layout per iteration (border=0):
1459
- * <name padded to nameWidth><header_nl_right or space>
1460
- * <" " or wrap_left><value chunk (padded to dwidth iff more follows)><wrap_right / nl_right>
1461
- *
1462
- * Layout per iteration (border=1):
1463
- * <name padded to nameWidth><header_nl_right or space>
1464
- * <vrule>
1465
- * <" " or wrap_left><value chunk><wrap_right / nl_right or trailing space>
1466
- *
1467
- * Layout per iteration (border=2): adds outer vrules, always pads the
1468
- * value column to dwidth, and always emits trailing markers.
1469
- *
1470
- * When header is done but data is not, the header side becomes pure
1471
- * whitespace of width `hwidth + opt_border (+ 1 for border-0 hmultiline)`.
1472
- * When data is done but header is not (because hheight > dheight), the
1473
- * data side is just `\n` at border<2, or `<dwidth spaces> |` at border>=2.
1474
- */
1475
- const renderVerticalCell = (headerLines, dataLines, nameWidth, dwidth, border, glyphs, hmultiline, emitHeaderMarker, emitDataMarker, emitHeaderLeftSlot, oldAscii) => {
1476
- const { vrule } = glyphs;
1477
- let out = "";
1478
- const dataChunks = [];
1479
- for (const src of dataLines) {
1480
- if (dwidth > 0 && displayWidth(src) > dwidth) {
1481
- const pieces = wrapLine(src, dwidth);
1482
- const lastIdx = pieces.length - 1;
1483
- pieces.forEach((piece, pi) => {
1484
- dataChunks.push({
1485
- text: piece,
1486
- width: displayWidth(piece),
1487
- isWrapStart: pi === 0,
1488
- isWrapEnd: pi === lastIdx,
1489
- });
1490
- });
1491
- }
1492
- else {
1493
- dataChunks.push({
1494
- text: src,
1495
- width: displayWidth(src),
1496
- isWrapStart: true,
1497
- isWrapEnd: true,
1498
- });
1499
- }
1500
- }
1501
- let hLine = 0;
1502
- let dLine = 0;
1503
- // `offset` mirrors upstream's `offset` variable (print.c line 1638): it
1504
- // tracks how much of the current data line has been emitted so far. A
1505
- // non-zero value at the start of an iteration means we're continuing a
1506
- // wrap (drives midvrule_wrap on old-ascii separator picking). Upstream
1507
- // updates `offset += bytes_to_output` even on the final chunk of a
1508
- // non-empty cell, so the next iteration (when header is still
1509
- // continuing) sees offset > 0.
1510
- let offset = 0;
1511
- let hcomplete = headerLines.length === 0;
1512
- let dcomplete = dataChunks.length === 0;
1513
- while (!hcomplete || !dcomplete) {
1514
- // ---- Left border ----
1515
- if (border === 2 || border === 3)
1516
- out += vrule;
1517
- // ---- Header part ----
1518
- if (!hcomplete) {
1519
- // Leading slot. Upstream `print.c` lines 1655-1657: emitted at
1520
- // `border==2 || (hmultiline && oldAscii)`. Old-ascii routes the
1521
- // continuation marker through the LEFT side via header_nl_left.
1522
- if (emitHeaderLeftSlot) {
1523
- out += hLine > 0 ? glyphs.headerNlLeft : " ";
1524
- }
1525
- const text = headerLines[hLine];
1526
- out += padToWidth(text, nameWidth, "left");
1527
- const hasMore = hLine + 1 < headerLines.length;
1528
- if (hasMore) {
1529
- if (emitHeaderMarker)
1530
- out += glyphs.headerNlRight;
1531
- hLine++;
1532
- }
1533
- else {
1534
- if (emitHeaderMarker)
1535
- out += " ";
1536
- hcomplete = true;
1537
- }
1538
- }
1539
- else {
1540
- // Header exhausted but data still has lines. Pad with
1541
- // `nameWidth + border` spaces, +1 at border<2 if hmultiline &&
1542
- // oldAscii (mirrors the lhwidth bump), +1 at border==0 if
1543
- // hmultiline && !oldAscii (mirrors the extra trailing slot the
1544
- // non-old-ascii branch carved out). Upstream print.c lines
1545
- // 1693-1707.
1546
- let swidth = nameWidth + border;
1547
- if (border < 2 && hmultiline && oldAscii)
1548
- swidth++;
1549
- if (border === 0 && hmultiline && !oldAscii)
1550
- swidth++;
1551
- out += " ".repeat(swidth);
1552
- }
1553
- // ---- Separator ----
1554
- // Border > 0 emits the column rule. Upstream `print.c` 1710-1719
1555
- // picks midvrule_wrap (`;` old-ascii) when offset != 0 (mid-wrap or
1556
- // we just finished emitting a non-empty cell), midvrule (`|`) on the
1557
- // very first data line, midvrule_nl (`:` old-ascii) on subsequent
1558
- // data lines. For ASCII/Unicode all three resolve to the same vrule
1559
- // glyph so the branch collapses; for old-ascii the continuation
1560
- // glyph drives the visual distinction.
1561
- if (border > 0) {
1562
- if (offset !== 0)
1563
- out += glyphs.midvruleWrap;
1564
- else if (dLine === 0)
1565
- out += vrule;
1566
- else
1567
- out += glyphs.midvruleNl;
1568
- }
1569
- // ---- Data part ----
1570
- if (!dcomplete) {
1571
- const chunk = dataChunks[dLine];
1572
- // Leading slot: " " on the first chunk of a source line,
1573
- // wrap_left on a wrap continuation. ALWAYS emitted (upstream
1574
- // print.c line 1731 has no border guard).
1575
- out += offset === 0 ? " " : glyphs.wrapLeft;
1576
- out += chunk.text;
1577
- // Mirror upstream's `offset += bytes_to_output`: bumped even on
1578
- // the last chunk of a cell, so the next iteration (header still
1579
- // continuing) sees offset > 0 and picks midvrule_wrap.
1580
- offset += chunk.width;
1581
- const isLastChunk = dLine === dataChunks.length - 1;
1582
- const nextChunk = !isLastChunk ? dataChunks[dLine + 1] : null;
1583
- let needsPad = false;
1584
- let markerGlyph = "";
1585
- if (nextChunk && !chunk.isWrapEnd) {
1586
- // Wrap continuation: next chunk continues THIS source line.
1587
- if (emitDataMarker) {
1588
- needsPad = true;
1589
- markerGlyph = glyphs.wrapRight;
1590
- }
1591
- dLine++;
1592
- // Offset stays bumped — next chunk continues the same source line.
1593
- }
1594
- else if (nextChunk) {
1595
- // Source-line boundary: next chunk starts a new data line.
1596
- if (emitDataMarker) {
1597
- needsPad = true;
1598
- markerGlyph = glyphs.nlRight;
1599
- }
1600
- dLine++;
1601
- offset = 0; // reset for new source line
1602
- }
1603
- else {
1604
- // End of cell.
1605
- if (border > 1) {
1606
- // Border 2/3: pad to dwidth, trailing space, then vrule.
1607
- needsPad = true;
1608
- markerGlyph = " ";
1609
- }
1610
- dcomplete = true;
1611
- // Offset stays bumped — header-tail iterations should see
1612
- // midvrule_wrap unless the cell was empty.
1613
- }
1614
- if (needsPad) {
1615
- const pad = dwidth - chunk.width;
1616
- if (pad > 0)
1617
- out += " ".repeat(pad);
1618
- }
1619
- out += markerGlyph;
1620
- // ---- Right border ----
1621
- if (border === 2 || border === 3)
1622
- out += vrule;
1623
- }
1624
- else {
1625
- // Data exhausted. Border<2 emits no further chars (a bare `\n`).
1626
- // Border>=2 pads the value column then closes with right vrule
1627
- // (upstream: `fprintf(fout, "%*s %s\n", dwidth, "", rightvrule)`
1628
- // — note the TWO trailing spaces between the dwidth pad and rule).
1629
- if (border === 2 || border === 3) {
1630
- out += " ".repeat(dwidth) + " " + vrule;
1631
- }
1632
- }
1633
- out += "\n";
1634
- }
1635
- return out;
1636
- };
1637
- // ---------------------------------------------------------------------------
1638
- // Mode selection.
1639
- // ---------------------------------------------------------------------------
1640
- const horizontalTotalWidth = (rs, topt) => {
1641
- const widths = computeColumnWidths(rs, topt);
1642
- const border = topt.border;
1643
- const n = widths.length;
1644
- // Match upstream `print.c` (lines 763-769) width_total:
1645
- // border 0: n; border 1: 3n-1; border 2/3: 3n+1.
1646
- let overhead;
1647
- if (border === 0)
1648
- overhead = n;
1649
- else if (border === 1)
1650
- overhead = n * 3 - (n > 0 ? 1 : 0);
1651
- else
1652
- overhead = n * 3 + 1;
1653
- return overhead + widths.reduce((a, b) => a + b, 0);
1654
- };
1655
- const chooseExpanded = (rs, topt) => {
1656
- if (topt.expanded === "on")
1657
- return true;
1658
- if (topt.expanded === "auto") {
1659
- const maxColumns = topt.columns > 0 ? topt.columns : topt.envColumns;
1660
- if (maxColumns <= 0)
1661
- return false;
1662
- return horizontalTotalWidth(rs, topt) > maxColumns;
1663
- }
1664
- return false;
1665
- };
1666
- // ---------------------------------------------------------------------------
1667
- // Public printer.
1668
- // ---------------------------------------------------------------------------
1669
- export const alignedPrinter = {
1670
- format: "aligned",
1671
- printQuery(rs, opts, out) {
1672
- const expanded = chooseExpanded(rs, opts.topt);
1673
- const wrapped = opts.topt.format === "wrapped";
1674
- let text;
1675
- if (expanded) {
1676
- text = renderVertical(rs, opts);
1677
- }
1678
- else if (rs.rows.length === 0) {
1679
- // Empty result: header + (0 rows) only, regardless of border.
1680
- text = renderEmpty(rs, opts);
1681
- }
1682
- else {
1683
- text = renderHorizontal(rs, opts, wrapped);
1684
- }
1685
- out.write(text);
1686
- return Promise.resolve();
1687
- },
1688
- };
1689
- const renderEmpty = (rs, opts) => {
1690
- // For an empty horizontal result, psql still prints the header row
1691
- // and rule, then the (0 rows) footer.
1692
- const topt = opts.topt;
1693
- const tuplesOnly = topt.tuplesOnly;
1694
- const border = topt.border;
1695
- const glyphs = glyphsFor(topt.unicodeBorderLineStyle);
1696
- const headerCells = rs.fields.map((f) => formatCell(f.name));
1697
- const widths = headerCells.map((c) => c.width);
1698
- let out = "";
1699
- if (!tuplesOnly && topt.title) {
1700
- // Centre the title above the table, matching the horizontal-path
1701
- // logic (and upstream `print_aligned_text` `width_total` formula).
1702
- // Falling through with no padding when title >= width keeps left-
1703
- // alignment for very wide titles — `\d <wide-named-table>` typical.
1704
- const overhead = border === 0
1705
- ? widths.length
1706
- : border === 1
1707
- ? widths.length * 3 - (widths.length > 0 ? 1 : 0)
1708
- : widths.length * 3 + 1;
1709
- const widthTotal = overhead + widths.reduce((a, b) => a + b, 0);
1710
- const titleW = displayWidth(topt.title);
1711
- if (titleW >= widthTotal) {
1712
- out += topt.title + "\n";
1713
- }
1714
- else {
1715
- out += " ".repeat((widthTotal - titleW) >> 1) + topt.title + "\n";
1716
- }
1717
- }
1718
- if (!tuplesOnly && (border === 2 || border === 3)) {
1719
- out += buildRule(widths, border, glyphs, "top") + "\n";
1720
- }
1721
- if (!tuplesOnly) {
1722
- const noneStates = headerCells.map(() => "none");
1723
- out +=
1724
- renderHeaderLine(headerCells.map((c) => c.lines[0] ?? ""), widths, border, glyphs, noneStates, noneStates, true) + "\n";
1725
- out += buildRule(widths, border, glyphs, "middle") + "\n";
1726
- }
1727
- if (!tuplesOnly && (border === 2 || border === 3)) {
1728
- out += buildRule(widths, border, glyphs, "bottom") + "\n";
1729
- }
1730
- // User footers suppress the default `(0 rows)` row counter — mirrors
1731
- // upstream `footers_with_default` (print.c lines 397-413).
1732
- const hasUserFooters = !!opts.footers && opts.footers.length > 0;
1733
- if (!tuplesOnly && topt.defaultFooter && !hasUserFooters) {
1734
- out += "(0 rows)\n";
1735
- }
1736
- if (!tuplesOnly && opts.footers) {
1737
- for (const f of opts.footers)
1738
- out += f + "\n";
1739
- }
1740
- // Trailing blank line between query results, mirroring the horizontal /
1741
- // vertical code paths (which append `\n` via `printTableCleanup`).
1742
- // Empty results were previously missing this separator, which made
1743
- // multiple back-to-back `\d`-family queries run together. Emitted
1744
- // unconditionally (matches `fputc('\n', fout)` on print.c line 1196 /
1745
- // 1821 — both are outside the `!opt_tuples_only` guard).
1746
- out += "\n";
1747
- return out;
1748
- };