neonctl 2.37.1 → 2.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +15 -700
  2. package/bin/cli.js +3 -0
  3. package/package.json +8 -75
  4. package/dist/analytics.js +0 -168
  5. package/dist/api.js +0 -720
  6. package/dist/auth.js +0 -125
  7. package/dist/callback.html +0 -60
  8. package/dist/cli.js +0 -9
  9. package/dist/commands/api.js +0 -278
  10. package/dist/commands/auth.js +0 -214
  11. package/dist/commands/bootstrap.js +0 -481
  12. package/dist/commands/branches.js +0 -488
  13. package/dist/commands/bucket.js +0 -549
  14. package/dist/commands/checkout.js +0 -321
  15. package/dist/commands/config.js +0 -626
  16. package/dist/commands/connection_string.js +0 -172
  17. package/dist/commands/data_api.js +0 -284
  18. package/dist/commands/databases.js +0 -82
  19. package/dist/commands/deploy.js +0 -26
  20. package/dist/commands/dev.js +0 -698
  21. package/dist/commands/diff.js +0 -222
  22. package/dist/commands/env.js +0 -189
  23. package/dist/commands/functions.js +0 -373
  24. package/dist/commands/index.js +0 -62
  25. package/dist/commands/init.js +0 -73
  26. package/dist/commands/inspect.js +0 -65
  27. package/dist/commands/ip_allow.js +0 -137
  28. package/dist/commands/link.js +0 -1121
  29. package/dist/commands/neon_auth.js +0 -1028
  30. package/dist/commands/operations.js +0 -28
  31. package/dist/commands/orgs.js +0 -24
  32. package/dist/commands/projects.js +0 -413
  33. package/dist/commands/psql.js +0 -62
  34. package/dist/commands/roles.js +0 -65
  35. package/dist/commands/schema_diff.js +0 -151
  36. package/dist/commands/set_context.js +0 -29
  37. package/dist/commands/snapshots.js +0 -455
  38. package/dist/commands/status.js +0 -40
  39. package/dist/commands/user.js +0 -15
  40. package/dist/commands/vpc_endpoints.js +0 -134
  41. package/dist/config.js +0 -11
  42. package/dist/config_format.js +0 -72
  43. package/dist/context.js +0 -231
  44. package/dist/current_branch_fast_path.js +0 -55
  45. package/dist/dev/env.js +0 -244
  46. package/dist/dev/functions.js +0 -70
  47. package/dist/dev/inputs.js +0 -63
  48. package/dist/dev/runtime.js +0 -146
  49. package/dist/env.js +0 -36
  50. package/dist/env_file.js +0 -159
  51. package/dist/errors.js +0 -80
  52. package/dist/functions_api.js +0 -48
  53. package/dist/help.js +0 -146
  54. package/dist/index.js +0 -240
  55. package/dist/log.js +0 -18
  56. package/dist/parameters.gen.js +0 -480
  57. package/dist/pkg.js +0 -25
  58. package/dist/psql/cli.js +0 -53
  59. package/dist/psql/command/cmd_cond.js +0 -437
  60. package/dist/psql/command/cmd_connect.js +0 -820
  61. package/dist/psql/command/cmd_copy.js +0 -1035
  62. package/dist/psql/command/cmd_describe.js +0 -1815
  63. package/dist/psql/command/cmd_format.js +0 -948
  64. package/dist/psql/command/cmd_io.js +0 -2193
  65. package/dist/psql/command/cmd_lo.js +0 -393
  66. package/dist/psql/command/cmd_meta.js +0 -969
  67. package/dist/psql/command/cmd_misc.js +0 -187
  68. package/dist/psql/command/cmd_pipeline.js +0 -1148
  69. package/dist/psql/command/cmd_restrict.js +0 -171
  70. package/dist/psql/command/cmd_show.js +0 -766
  71. package/dist/psql/command/dispatch.js +0 -343
  72. package/dist/psql/command/inputQueue.js +0 -42
  73. package/dist/psql/command/shared.js +0 -71
  74. package/dist/psql/complete/filenames.js +0 -139
  75. package/dist/psql/complete/index.js +0 -104
  76. package/dist/psql/complete/matcher.js +0 -315
  77. package/dist/psql/complete/psqlVars.js +0 -249
  78. package/dist/psql/complete/queries.js +0 -493
  79. package/dist/psql/complete/rules.js +0 -2424
  80. package/dist/psql/core/common.js +0 -1253
  81. package/dist/psql/core/help.js +0 -576
  82. package/dist/psql/core/mainloop.js +0 -1360
  83. package/dist/psql/core/prompt.js +0 -439
  84. package/dist/psql/core/settings.js +0 -686
  85. package/dist/psql/core/sqlHelp.js +0 -1066
  86. package/dist/psql/core/startup.js +0 -846
  87. package/dist/psql/core/syncVars.js +0 -116
  88. package/dist/psql/core/variables.js +0 -287
  89. package/dist/psql/describe/formatters.js +0 -1330
  90. package/dist/psql/describe/processNamePattern.js +0 -270
  91. package/dist/psql/describe/queries.js +0 -2452
  92. package/dist/psql/describe/versionGate.js +0 -44
  93. package/dist/psql/index.js +0 -2030
  94. package/dist/psql/io/history.js +0 -299
  95. package/dist/psql/io/input.js +0 -120
  96. package/dist/psql/io/lineEditor/buffer.js +0 -325
  97. package/dist/psql/io/lineEditor/complete.js +0 -227
  98. package/dist/psql/io/lineEditor/filename.js +0 -159
  99. package/dist/psql/io/lineEditor/index.js +0 -893
  100. package/dist/psql/io/lineEditor/keymap.js +0 -745
  101. package/dist/psql/io/lineEditor/vt100.js +0 -363
  102. package/dist/psql/io/pgpass.js +0 -202
  103. package/dist/psql/io/pgservice.js +0 -194
  104. package/dist/psql/io/psqlrc.js +0 -422
  105. package/dist/psql/print/aligned.js +0 -1748
  106. package/dist/psql/print/asciidoc.js +0 -230
  107. package/dist/psql/print/crosstab.js +0 -463
  108. package/dist/psql/print/csv.js +0 -76
  109. package/dist/psql/print/html.js +0 -240
  110. package/dist/psql/print/json.js +0 -96
  111. package/dist/psql/print/latex.js +0 -379
  112. package/dist/psql/print/pager.js +0 -267
  113. package/dist/psql/print/troff.js +0 -240
  114. package/dist/psql/print/unaligned.js +0 -99
  115. package/dist/psql/print/units.js +0 -188
  116. package/dist/psql/scanner/slash.js +0 -515
  117. package/dist/psql/scanner/sql.js +0 -914
  118. package/dist/psql/scanner/stringutils.js +0 -394
  119. package/dist/psql/types/backslash.js +0 -1
  120. package/dist/psql/types/connection.js +0 -1
  121. package/dist/psql/types/index.js +0 -7
  122. package/dist/psql/types/printer.js +0 -1
  123. package/dist/psql/types/repl.js +0 -1
  124. package/dist/psql/types/scanner.js +0 -24
  125. package/dist/psql/types/settings.js +0 -1
  126. package/dist/psql/types/variables.js +0 -1
  127. package/dist/psql/wire/connection.js +0 -2857
  128. package/dist/psql/wire/copy.js +0 -108
  129. package/dist/psql/wire/notify.js +0 -59
  130. package/dist/psql/wire/pipeline.js +0 -521
  131. package/dist/psql/wire/protocol.js +0 -466
  132. package/dist/psql/wire/sasl.js +0 -294
  133. package/dist/psql/wire/tls.js +0 -602
  134. package/dist/storage_api.js +0 -147
  135. package/dist/test_utils/fixtures.js +0 -121
  136. package/dist/test_utils/oauth_server.js +0 -9
  137. package/dist/types.js +0 -1
  138. package/dist/utils/ai_gateway_notice.js +0 -180
  139. package/dist/utils/api_enums.js +0 -33
  140. package/dist/utils/auth.js +0 -5
  141. package/dist/utils/branch_notice.js +0 -22
  142. package/dist/utils/branch_picker.js +0 -103
  143. package/dist/utils/compute_units.js +0 -28
  144. package/dist/utils/config_diff.js +0 -185
  145. package/dist/utils/enrichers.js +0 -161
  146. package/dist/utils/esbuild.js +0 -158
  147. package/dist/utils/formats.js +0 -18
  148. package/dist/utils/git_diff.js +0 -90
  149. package/dist/utils/inspect_db.js +0 -153
  150. package/dist/utils/inspect_queries.js +0 -372
  151. package/dist/utils/middlewares.js +0 -20
  152. package/dist/utils/openapi.js +0 -114
  153. package/dist/utils/package_manager.js +0 -68
  154. package/dist/utils/point_in_time.js +0 -56
  155. package/dist/utils/psql.js +0 -120
  156. package/dist/utils/string.js +0 -5
  157. package/dist/utils/ui.js +0 -59
  158. package/dist/utils/zip.js +0 -4
  159. package/dist/writer.js +0 -97
@@ -1,230 +0,0 @@
1
- import { boolDisplayOf, renderCellValue } from "./units.js";
2
- /**
3
- * AsciiDoc printer.
4
- *
5
- * Mirrors print.c `print_asciidoc_text` and `print_asciidoc_vertical`.
6
- *
7
- * Output shape (flat):
8
- * .title
9
- * [options="header",cols="<l,<l,>l",frame="all",grid="all"]
10
- * |====
11
- * ^l|col1 ^l|col2 ^l|col3
12
- * |val1 |val2 |val3
13
- * |====
14
- *
15
- * ....
16
- * (N rows)
17
- * ....
18
- *
19
- * Output shape (expanded, `\pset expanded on`):
20
- * .title
21
- * [cols="h,l",frame="all",grid="all"]
22
- * |====
23
- * 2+^|Record 1
24
- * <l|col1 <l|val1
25
- * <l|col2 >l|val2
26
- * ...
27
- * |====
28
- * (no row count footer)
29
- *
30
- * - `topt.border`:
31
- * 0 → `,frame="none",grid="none"`
32
- * 1 → `,frame="none"`
33
- * 2 → `,frame="all",grid="all"`
34
- * Borders outside 0..2 fall back to "no extra clause" (matches the
35
- * switch with no default in print.c).
36
- * - Numeric columns (per the OID heuristic) get `>l`, others `<l`.
37
- * Headers in flat mode are centered with `^l|`. In expanded mode
38
- * header cells render with `<l|` and value cells use the alignment
39
- * letter from the data column.
40
- * - AsciiDoc's only structurally hostile character is `|` (it ends a
41
- * cell), so the escape helper only rewrites that to `\|`. Newlines
42
- * pass through — AsciiDoc continues a cell over an embedded newline.
43
- * - Whitespace-only cells are emitted as just `|` (with a trailing
44
- * space for inter-cell separation), matching the upstream
45
- * "protect against needless spaces" branch.
46
- * - Expanded mode never emits the `(N rows)` footer; only user-set
47
- * footers appear (matches upstream `print_asciidoc_vertical`).
48
- */
49
- // INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC, INTERVAL.
50
- const NUMERIC_OIDS = new Set([21, 23, 20, 700, 701, 1700, 1186]);
51
- export const asciidocPrinter = {
52
- format: "asciidoc",
53
- printQuery(rs, opts, out) {
54
- const topt = opts.topt;
55
- if (topt.expanded === "on") {
56
- return printExpanded(rs, opts, out);
57
- }
58
- return printFlat(rs, opts, out);
59
- },
60
- };
61
- const printFlat = (rs, opts, out) => {
62
- const topt = opts.topt;
63
- const tuplesOnly = topt.tuplesOnly;
64
- const startTable = topt.startTable;
65
- const stopTable = topt.stopTable;
66
- const nullPrint = opts.nullPrint !== "" ? opts.nullPrint : topt.nullPrint;
67
- const title = opts.title ?? topt.title;
68
- const footers = opts.footers ?? topt.footers;
69
- const headers = rs.fields.map((f) => f.name);
70
- const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? "r" : "l");
71
- const ncols = rs.fields.length;
72
- const boolDisplay = boolDisplayOf(topt);
73
- const cells = rs.rows.map((row) => row.map((cell, i) => renderCell(cell, nullPrint, topt.numericLocale, rs.fields[i]?.dataTypeID, boolDisplay)));
74
- let buf = "";
75
- if (startTable) {
76
- // Force a paragraph break (upstream always emits a leading "\n").
77
- buf += "\n";
78
- if (!tuplesOnly && title) {
79
- buf += "." + title + "\n";
80
- }
81
- buf += "[";
82
- if (!tuplesOnly)
83
- buf += 'options="header",';
84
- buf += 'cols="';
85
- buf += aligns.map((a) => (a === "r" ? ">l" : "<l")).join(",");
86
- buf += '"';
87
- buf += borderClause(topt.border);
88
- buf += "]\n";
89
- buf += "|====\n";
90
- if (!tuplesOnly) {
91
- headers.forEach((h, idx) => {
92
- if (idx !== 0)
93
- buf += " ";
94
- buf += "^l|" + escapeAsciidoc(h);
95
- });
96
- buf += "\n";
97
- }
98
- }
99
- for (const row of cells) {
100
- row.forEach((value, idx) => {
101
- if (idx !== 0)
102
- buf += " ";
103
- buf += "|";
104
- if (isWhitespaceOnly(value)) {
105
- // The upstream code emits a trailing space only for cells
106
- // that are not the last in their row.
107
- if (idx !== ncols - 1)
108
- buf += " ";
109
- }
110
- else {
111
- buf += escapeAsciidoc(value);
112
- }
113
- });
114
- buf += "\n";
115
- }
116
- buf += "|====\n";
117
- if (stopTable && !tuplesOnly) {
118
- const effective = effectiveFooters(rs, topt, footers);
119
- if (effective.length > 0) {
120
- buf += "\n....\n";
121
- for (const f of effective)
122
- buf += f + "\n";
123
- buf += "....\n";
124
- }
125
- }
126
- out.write(buf);
127
- return Promise.resolve();
128
- };
129
- const printExpanded = (rs, opts, out) => {
130
- const topt = opts.topt;
131
- const tuplesOnly = topt.tuplesOnly;
132
- const startTable = topt.startTable;
133
- const stopTable = topt.stopTable;
134
- const nullPrint = opts.nullPrint !== "" ? opts.nullPrint : topt.nullPrint;
135
- const title = opts.title ?? topt.title;
136
- const footers = opts.footers ?? topt.footers;
137
- const headers = rs.fields.map((f) => f.name);
138
- const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? "r" : "l");
139
- const boolDisplay = boolDisplayOf(topt);
140
- const cells = rs.rows.map((row) => row.map((cell, i) => renderCell(cell, nullPrint, topt.numericLocale, rs.fields[i]?.dataTypeID, boolDisplay)));
141
- let buf = "";
142
- if (startTable) {
143
- buf += "\n";
144
- if (!tuplesOnly && title) {
145
- buf += "." + title + "\n";
146
- }
147
- buf += '[cols="h,l"';
148
- buf += borderClause(topt.border);
149
- buf += "]\n";
150
- buf += "|====\n";
151
- }
152
- let record = topt.prior + 1;
153
- cells.forEach((row) => {
154
- if (!tuplesOnly) {
155
- buf += `2+^|Record ${String(record)}\n`;
156
- record += 1;
157
- }
158
- else {
159
- buf += "2+|\n";
160
- }
161
- row.forEach((value, idx) => {
162
- buf += "<l|" + escapeAsciidoc(headers[idx]);
163
- buf += " " + (aligns[idx] === "r" ? ">l" : "<l") + "|";
164
- if (isWhitespaceOnly(value)) {
165
- buf += " ";
166
- }
167
- else {
168
- buf += escapeAsciidoc(value);
169
- }
170
- buf += "\n";
171
- });
172
- });
173
- buf += "|====\n";
174
- if (stopTable && !tuplesOnly) {
175
- // Expanded mode does NOT emit the default "(N rows)" footer —
176
- // only user-supplied footers (matches print_asciidoc_vertical).
177
- if (footers && footers.length > 0) {
178
- buf += "\n....\n";
179
- for (const f of footers)
180
- buf += f + "\n";
181
- buf += "....\n";
182
- }
183
- }
184
- out.write(buf);
185
- return Promise.resolve();
186
- };
187
- const borderClause = (border) => {
188
- switch (border) {
189
- case 0:
190
- return ',frame="none",grid="none"';
191
- case 1:
192
- return ',frame="none"';
193
- case 2:
194
- return ',frame="all",grid="all"';
195
- default:
196
- return "";
197
- }
198
- };
199
- const effectiveFooters = (rs, topt, footers) => {
200
- if (footers && footers.length > 0)
201
- return footers;
202
- if (topt.defaultFooter) {
203
- const n = rs.rows.length;
204
- return [`(${String(n)} ${n === 1 ? "row" : "rows"})`];
205
- }
206
- return [];
207
- };
208
- const isWhitespaceOnly = (s) => {
209
- if (s.length === 0)
210
- return true;
211
- for (const ch of s) {
212
- if (ch !== " " && ch !== "\t")
213
- return false;
214
- }
215
- return true;
216
- };
217
- const escapeAsciidoc = (input) => {
218
- // Only `|` is structurally hostile (closes a cell). Newlines and
219
- // every other character pass through; AsciiDoc treats embedded `\n`
220
- // as a soft line break within a cell.
221
- let out = "";
222
- for (const ch of input) {
223
- if (ch === "|")
224
- out += "\\|";
225
- else
226
- out += ch;
227
- }
228
- return out;
229
- };
230
- const renderCell = (cell, nullPrint, numericLocale, dataTypeID, boolDisplay) => renderCellValue(cell, nullPrint, numericLocale, dataTypeID, boolDisplay);
@@ -1,463 +0,0 @@
1
- /**
2
- * psql `\crosstabview` pivot + render (WP-22).
3
- *
4
- * TypeScript port of `src/bin/psql/crosstabview.c`:
5
- *
6
- * - `pivotResultSet` is the moral equivalent of `PrintResultInCrosstab`'s
7
- * "first/second/third part" — resolve the four column references against
8
- * the source ResultSet's fields, collect the distinct vertical / horizontal
9
- * header values (in first-appearance order, matching upstream's AVL-tree
10
- * `rank = tree->count` assignment), detect duplicate `(colV, colH)` pairs,
11
- * and produce a new ResultSet whose first column is the vertical header
12
- * and whose remaining columns are one per horizontal-header value.
13
- *
14
- * - `printCrosstab` runs the pivot and then forwards the pivoted ResultSet
15
- * to the existing aligned printer (`alignedPrinter.printQuery`). We
16
- * deliberately reuse the aligned printer so border / nullPrint / locale /
17
- * unicode glyphs / expanded all keep working without re-implementation.
18
- *
19
- * Column references accept upstream's two forms — a 1-based column number
20
- * (`"1"`, `"2"`, ...) and a column name (matched against `ResultSet.fields`
21
- * using upstream's "dequote then downcase" rule). The fourth argument
22
- * (`sortColH`) additionally honours a leading `+` / `-` to request
23
- * ascending / descending sort on the horizontal header values — a small
24
- * extension over upstream's "sort by the sort-column's payload" semantics
25
- * that lets callers pivot without an explicit numeric sort column.
26
- *
27
- * Error cases (text matches `pg_log_error` strings from crosstabview.c):
28
- *
29
- * - "query must return at least three columns",
30
- * - "vertical and horizontal headers must be different columns",
31
- * - "column number N is out of range 1..M",
32
- * - "column name not found: \"…\"",
33
- * - "ambiguous column name: \"…\"",
34
- * - "maximum number of columns (1600) exceeded",
35
- * - "query result contains multiple data values for row \"…\", column \"…\"".
36
- */
37
- import { alignedPrinter } from "./aligned.js";
38
- const DIGIT_RE = /^\d+$/;
39
- const SIGNED_DIGIT_RE = /^[+-]?\d+$/;
40
- const dequoteDowncase = (raw) => {
41
- let out = "";
42
- let inquotes = false;
43
- let hadQuotes = false;
44
- let i = 0;
45
- while (i < raw.length) {
46
- const c = raw[i];
47
- if (c === '"') {
48
- hadQuotes = true;
49
- if (inquotes && raw[i + 1] === '"') {
50
- out += '"';
51
- i += 2;
52
- continue;
53
- }
54
- inquotes = !inquotes;
55
- i++;
56
- continue;
57
- }
58
- out += inquotes ? c : c.toLowerCase();
59
- i++;
60
- }
61
- return { value: out, hadQuotes };
62
- };
63
- const indexOfColumn = (arg, fields, allowSign) => {
64
- // Numeric arg path: number type, or string that parses as integer.
65
- if (typeof arg === "number") {
66
- if (!Number.isInteger(arg)) {
67
- return {
68
- ok: false,
69
- error: `column number ${String(arg)} is not an integer`,
70
- };
71
- }
72
- const sign = arg < 0 ? -1 : 1;
73
- const abs = Math.abs(arg);
74
- if (abs < 1 || abs > fields.length) {
75
- return {
76
- ok: false,
77
- error: `column number ${String(abs)} is out of range 1..${String(fields.length)}`,
78
- };
79
- }
80
- return { ok: true, index: abs - 1, sign: allowSign ? sign : 1 };
81
- }
82
- let str = arg.trim();
83
- let sign = 1;
84
- if (allowSign && (str.startsWith("+") || str.startsWith("-"))) {
85
- if (str.startsWith("-"))
86
- sign = -1;
87
- // Peel only when the rest looks like it could be a referencing token.
88
- const rest = str.slice(1);
89
- if (rest.length > 0)
90
- str = rest;
91
- }
92
- if (str.length === 0) {
93
- return { ok: false, error: "empty column reference" };
94
- }
95
- if (DIGIT_RE.test(str)) {
96
- const n = parseInt(str, 10);
97
- if (n < 1 || n > fields.length) {
98
- return {
99
- ok: false,
100
- error: `column number ${String(n)} is out of range 1..${String(fields.length)}`,
101
- };
102
- }
103
- return { ok: true, index: n - 1, sign };
104
- }
105
- // Name lookup: upstream `indexOfColumn` dequotes & downcases the arg
106
- // in place, then runs `strcmp(arg, PQfname(res, i))` against the raw
107
- // field name. The field name is NOT itself downcased, so:
108
- // - unquoted `B` → "b" → matches field `b` only (not `B`);
109
- // - unquoted `Foo` → "foo" → does NOT match field `Foo`;
110
- // - quoted `"B"` → "B" → matches field `B` only;
111
- // - quoted `"Foo"` → "Foo" → matches field `Foo`.
112
- // No case-insensitive fallback — that mismatched the
113
- // "need to quote name" test in the conformance corpus.
114
- const { value: needle } = dequoteDowncase(str);
115
- let found = -1;
116
- for (let i = 0; i < fields.length; i++) {
117
- if (fields[i].name === needle) {
118
- if (found >= 0) {
119
- // Upstream's `indexOfColumn` formats the dequoted/downcased name
120
- // in the error message, not the raw arg with its leading
121
- // quotes. Matches `pg_log_error("ambiguous column name: \"%s\"")`
122
- // after the in-place `dequote_downcase_identifier(arg)` mutation.
123
- return {
124
- ok: false,
125
- error: `ambiguous column name: "${needle}"`,
126
- };
127
- }
128
- found = i;
129
- }
130
- }
131
- if (found === -1) {
132
- // Same convention as the ambiguous-name branch: format the
133
- // dequoted/downcased name so quoted `"B"` reads as `"B"` and
134
- // unquoted `Foo` reads as `"foo"`.
135
- return { ok: false, error: `column name not found: "${needle}"` };
136
- }
137
- return { ok: true, index: found, sign };
138
- };
139
- // ---------------------------------------------------------------------------
140
- // Cell value → comparable key + display string
141
- // ---------------------------------------------------------------------------
142
- /**
143
- * Build a stable string key for a header value so we can deduplicate
144
- * vertical / horizontal headers consistently. We use `JSON.stringify`-ish
145
- * encoding plus a leading discriminator so e.g. the string `"1"` and the
146
- * number `1` don't collide (matches upstream where PQgetvalue returns a
147
- * type-specific text form — the rows pre-serialised by the server are
148
- * compared byte-for-byte).
149
- *
150
- * Null is represented by a sentinel so the printer can substitute
151
- * `popt.nullPrint`.
152
- */
153
- const NULL_SENTINEL = Symbol.for("neonctl.psql.crosstab.null");
154
- const headerKey = (value) => {
155
- if (value === null || value === undefined)
156
- return NULL_SENTINEL;
157
- if (typeof value === "string")
158
- return `s:${value}`;
159
- if (typeof value === "number")
160
- return `n:${String(value)}`;
161
- if (typeof value === "bigint")
162
- return `i:${String(value)}`;
163
- if (typeof value === "boolean")
164
- return `b:${String(value)}`;
165
- if (value instanceof Date)
166
- return `d:${value.toISOString()}`;
167
- if (value instanceof Uint8Array) {
168
- let hex = "x:";
169
- for (const b of value)
170
- hex += b.toString(16).padStart(2, "0");
171
- return hex;
172
- }
173
- return `j:${JSON.stringify(value)}`;
174
- };
175
- /**
176
- * Display form for a header value — used to populate the synthetic
177
- * ResultSet's `fields[i].name` (which must be a string, not nullable).
178
- * Mirrors upstream's `colname = piv_columns[…].name ? … : popt.nullPrint`.
179
- */
180
- const headerDisplay = (value, nullPrint) => {
181
- if (value === null || value === undefined)
182
- return nullPrint;
183
- if (typeof value === "string")
184
- return value;
185
- if (typeof value === "number" || typeof value === "bigint") {
186
- return value.toString();
187
- }
188
- if (typeof value === "boolean")
189
- return value ? "t" : "f";
190
- if (value instanceof Date)
191
- return value.toISOString();
192
- if (value instanceof Uint8Array) {
193
- let hex = "\\x";
194
- for (const b of value)
195
- hex += b.toString(16).padStart(2, "0");
196
- return hex;
197
- }
198
- return JSON.stringify(value);
199
- };
200
- /**
201
- * Compare two cells under `+`/`-` numeric semantics: if both look like
202
- * `/^-?\d+$/` (matching upstream's `rankSort` regex), compare as integers;
203
- * otherwise compare as strings. Nulls sort last. The `sign` flag flips the
204
- * order (descending).
205
- */
206
- const cmpForSort = (a, b, sign) => {
207
- const aNull = a === null || a === undefined;
208
- const bNull = b === null || b === undefined;
209
- if (aNull && bNull)
210
- return 0;
211
- if (aNull)
212
- return 1; // null last
213
- if (bNull)
214
- return -1;
215
- const aStr = typeof a === "string" ? a : headerDisplay(a, "");
216
- const bStr = typeof b === "string" ? b : headerDisplay(b, "");
217
- if (SIGNED_DIGIT_RE.test(aStr) && SIGNED_DIGIT_RE.test(bStr)) {
218
- const an = parseInt(aStr, 10);
219
- const bn = parseInt(bStr, 10);
220
- if (an < bn)
221
- return -1 * sign;
222
- if (an > bn)
223
- return 1 * sign;
224
- return 0;
225
- }
226
- if (aStr < bStr)
227
- return -1 * sign;
228
- if (aStr > bStr)
229
- return 1 * sign;
230
- return 0;
231
- };
232
- /**
233
- * Pivot a ResultSet into a `{ rowHeaders, colHeaders, matrix }` shape,
234
- * returning a synthetic ResultSet ready for the aligned printer.
235
- *
236
- * Detailed algorithm:
237
- *
238
- * 1. Resolve `colV` / `colH` / `colD` / `sortColH` to zero-based field
239
- * indices via {@link indexOfColumn}. `colV` defaults to 0, `colH` to 1.
240
- * `colD` defaults to the only remaining column when there are exactly
241
- * three; otherwise we require it explicitly (matching upstream).
242
- * 2. Walk rows once. For each row:
243
- * - `vKey` = header-key of the row's colV cell;
244
- * - `hKey` = header-key of the row's colH cell;
245
- * - If we haven't seen `vKey` or `hKey` before, give them the next
246
- * rank in first-appearance order, and capture the sort value
247
- * (from colSort) for hKey.
248
- * - Stash the cell at `(vRank, hRank)`. If a value already lives
249
- * there, surface a duplicate-pair error.
250
- * 3. If `sortColH` was supplied, stable-sort the horizontal header entries
251
- * by their captured sort value (numeric if both look numeric, else
252
- * string; `sign` for ascending/descending) and reassign ranks.
253
- * 4. Construct the synthetic ResultSet: one field for colV (carries colV's
254
- * original FieldDescription) + one field per horizontal header value
255
- * (each carrying colD's FieldDescription, so the aligned printer's
256
- * right-align heuristic kicks in for numeric data).
257
- * 5. Rows: for each vertical header in rank order, emit `[vHeaderValue,
258
- * ...cellsByHorizontalRank]`. Unfilled cells are `""` so the printer
259
- * just emits empty padding (matching upstream's
260
- * "non-initialized cells must be set to an empty string" pass).
261
- */
262
- export const pivotResultSet = (rs, opts,
263
- /**
264
- * Substitution string for `null` header values when synthesising the
265
- * pivoted FieldDescription.name. Upstream's `\crosstabview` formats a
266
- * NULL horizontal header using the current `\pset null` setting (an
267
- * empty string by default). Callers that don't care can omit; tests
268
- * that drive the function in isolation can supply a sentinel.
269
- */
270
- nullPrint = "") => {
271
- // (1) Field resolution. Upstream `crosstabview.c` requires PQnfields >= 3
272
- // unconditionally — pivoting two columns is degenerate (V × H with no
273
- // payload). Match the error text verbatim so the conformance test sees
274
- // the same line.
275
- if (rs.fields.length < 3) {
276
- return { error: "query must return at least three columns" };
277
- }
278
- const colV = opts.colV ?? 1;
279
- const colH = opts.colH ?? 2;
280
- const vRes = indexOfColumn(colV, rs.fields, false);
281
- if (!vRes.ok)
282
- return { error: vRes.error };
283
- const hRes = indexOfColumn(colH, rs.fields, false);
284
- if (!hRes.ok)
285
- return { error: hRes.error };
286
- if (vRes.index === hRes.index) {
287
- return {
288
- error: "vertical and horizontal headers must be different columns",
289
- };
290
- }
291
- let dataIdx;
292
- if (opts.colD === undefined) {
293
- // With exactly three columns and no explicit `colD`, the data column
294
- // is the remaining one. With more than three, upstream picks the
295
- // first non-V/H column too — we mirror that here so `SELECT v,h,c,i`
296
- // pivots `c` by default.
297
- let candidate = -1;
298
- for (let i = 0; i < rs.fields.length; i++) {
299
- if (i !== vRes.index && i !== hRes.index) {
300
- candidate = i;
301
- break;
302
- }
303
- }
304
- if (candidate < 0) {
305
- return { error: "no data column available" };
306
- }
307
- dataIdx = candidate;
308
- }
309
- else {
310
- const dRes = indexOfColumn(opts.colD, rs.fields, false);
311
- if (!dRes.ok)
312
- return { error: dRes.error };
313
- dataIdx = dRes.index;
314
- }
315
- let sortIdx = -1;
316
- let sortSign = 1;
317
- if (opts.sortColH !== undefined) {
318
- const sRes = indexOfColumn(opts.sortColH, rs.fields, true);
319
- if (!sRes.ok)
320
- return { error: sRes.error };
321
- sortIdx = sRes.index;
322
- sortSign = sRes.sign;
323
- }
324
- // (2) Single-pass row walk: build distinct V/H header sets in
325
- // first-appearance order and populate the data matrix.
326
- const vHeaders = new Map();
327
- const hHeaders = new Map();
328
- // matrix keyed by `${vRank}|${hRank}` rather than a 2D array because we
329
- // don't know the final dimensions until we've walked all rows. The
330
- // string key keeps lookups O(1) without packing into an array.
331
- const matrix = new Map();
332
- for (const row of rs.rows) {
333
- const vVal = row[vRes.index];
334
- const hVal = row[hRes.index];
335
- const dVal = row[dataIdx];
336
- const vk = headerKey(vVal);
337
- const hk = headerKey(hVal);
338
- let vEntry = vHeaders.get(vk);
339
- if (!vEntry) {
340
- vEntry = {
341
- key: vk,
342
- value: vVal,
343
- rank: vHeaders.size,
344
- sortValue: null,
345
- };
346
- vHeaders.set(vk, vEntry);
347
- }
348
- let hEntry = hHeaders.get(hk);
349
- if (!hEntry) {
350
- // Upstream `crosstabview.c` caps distinct horizontal-header values
351
- // at `CROSSTABVIEW_MAX_COLUMNS` (1600). Past that, the synthesised
352
- // result wouldn't be printable in a reasonable width anyway, so
353
- // we mirror the cap and the error text verbatim.
354
- if (hHeaders.size >= 1600) {
355
- return { error: "maximum number of columns (1600) exceeded" };
356
- }
357
- hEntry = {
358
- key: hk,
359
- value: hVal,
360
- rank: hHeaders.size,
361
- sortValue: sortIdx >= 0 ? row[sortIdx] : null,
362
- };
363
- hHeaders.set(hk, hEntry);
364
- }
365
- const cellKey = `${String(vEntry.rank)}|${String(hEntry.rank)}`;
366
- if (matrix.has(cellKey)) {
367
- const vDisp = headerDisplay(vEntry.value, "(null)");
368
- const hDisp = headerDisplay(hEntry.value, "(null)");
369
- return {
370
- error: `query result contains multiple data values for row "${vDisp}", column "${hDisp}"`,
371
- };
372
- }
373
- matrix.set(cellKey, dVal);
374
- }
375
- // (3) Sort horizontal headers if requested. We stable-sort by capturing
376
- // the original rank to break ties (and by using Array.prototype.sort
377
- // which is stable in V8/Node).
378
- //
379
- // We deliberately DO NOT mutate `HeaderEntry.rank` here — the matrix is
380
- // keyed by `${vRank}|${origHRank}` and changing `hArr[i].rank` to the
381
- // post-sort position would desynchronise lookups in step 5. Display
382
- // order is carried implicitly by the array index.
383
- const hArr = Array.from(hHeaders.values());
384
- if (sortIdx >= 0) {
385
- hArr.sort((a, b) => {
386
- const c = cmpForSort(a.sortValue, b.sortValue, sortSign);
387
- if (c !== 0)
388
- return c;
389
- // Tie-break on first-appearance rank to keep ordering deterministic.
390
- return a.rank - b.rank;
391
- });
392
- }
393
- const vArr = Array.from(vHeaders.values()).sort((a, b) => a.rank - b.rank);
394
- // (4) Build the synthetic ResultSet. Use the caller-supplied
395
- // `nullPrint` for any horizontal header whose source value was NULL —
396
- // upstream `do_crosstabview` calls `PQgetvalue(res, …)` which returns
397
- // the empty string for null, but then formats the header through
398
- // `popt.nullPrint` when the source cell was actually null. Pass the
399
- // active `\pset null` string in so `--null='#null#'` lights up the
400
- // last column header on a pivot with a NULL H value.
401
- const newFields = [
402
- {
403
- ...rs.fields[vRes.index],
404
- // Keep colV's name as-is for the row-header column.
405
- },
406
- ...hArr.map((h) => ({
407
- ...rs.fields[dataIdx],
408
- // Headers come from H values; we serialise to strings so the
409
- // FieldDescription.name contract (string, not unknown) is satisfied.
410
- name: headerDisplay(h.value, nullPrint),
411
- })),
412
- ];
413
- // (5) Emit rows in vertical-header rank order. Each row is
414
- // [vHeaderValue, ...cellsByHorizontalRank]. Unfilled cells become "".
415
- const newRows = vArr.map((v) => {
416
- const row = new Array(hArr.length + 1);
417
- row[0] = v.value;
418
- for (let i = 0; i < hArr.length; i++) {
419
- const key = `${String(v.rank)}|${String(hArr[i].rank)}`;
420
- // Unfilled cells: empty string so the aligned printer emits nothing
421
- // (rather than substituting nullPrint). Matches upstream's
422
- // "non-initialized cells must be set to an empty string" pass.
423
- row[i + 1] = matrix.has(key) ? matrix.get(key) : "";
424
- }
425
- return row;
426
- });
427
- const synthetic = {
428
- command: rs.command,
429
- rowCount: vArr.length,
430
- oid: null,
431
- fields: newFields,
432
- rows: newRows,
433
- notices: [],
434
- };
435
- return { rs: synthetic };
436
- };
437
- // ---------------------------------------------------------------------------
438
- // printCrosstab — pivot + delegate to alignedPrinter
439
- // ---------------------------------------------------------------------------
440
- /**
441
- * High-level entry: pivot `rs` per `opts`, then forward the synthetic
442
- * ResultSet to `alignedPrinter.printQuery`. Returns a `CrosstabError` if
443
- * pivoting fails; on success returns `undefined` (matching the rest of the
444
- * `print/` API surface).
445
- *
446
- * We deliberately use the aligned printer regardless of `printOpts.topt.format`:
447
- * upstream psql's `\crosstabview` always renders through the table printer
448
- * (it ignores `\pset format` for the duration of the call), and our tests
449
- * lean on the aligned printer's borders / nullPrint / numericLocale handling
450
- * matching exactly.
451
- */
452
- export const printCrosstab = async (rs, opts, printOpts, out) => {
453
- // Thread the active `\pset null` value through so a NULL horizontal
454
- // header renders as e.g. `#null#` in the column header row (mirroring
455
- // the way the aligned printer would otherwise render it for a body
456
- // cell). Without this, the synthetic FieldDescription.name comes out
457
- // as the empty string and the column header is just whitespace.
458
- const result = pivotResultSet(rs, opts, printOpts.topt.nullPrint);
459
- if ("error" in result)
460
- return result;
461
- await alignedPrinter.printQuery(result.rs, printOpts, out);
462
- return undefined;
463
- };