neon 3.0.0 → 3.1.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 (198) hide show
  1. package/README.md +54 -0
  2. package/dist/_shared/auth_selection.js +76 -79
  3. package/dist/_shared/credentials.js +166 -187
  4. package/dist/_shared/env-core/env.js +354 -517
  5. package/dist/_shared/env-core/reuse-secrets.js +159 -203
  6. package/dist/_shared/paths.js +129 -126
  7. package/dist/_shared/profiles.js +192 -242
  8. package/dist/_shared/secure_file.js +36 -38
  9. package/dist/_virtual/_rolldown/runtime.js +13 -0
  10. package/dist/analytics.js +163 -207
  11. package/dist/api.js +815 -758
  12. package/dist/auth.js +121 -141
  13. package/dist/auth_context.js +39 -53
  14. package/dist/cli.js +4 -7
  15. package/dist/commands/api.js +220 -250
  16. package/dist/commands/api_keys.js +251 -314
  17. package/dist/commands/auth.js +283 -328
  18. package/dist/commands/bootstrap.js +372 -437
  19. package/dist/commands/branches.js +304 -455
  20. package/dist/commands/bucket.js +374 -514
  21. package/dist/commands/checkout.js +213 -298
  22. package/dist/commands/config.js +573 -690
  23. package/dist/commands/connection_string.js +137 -165
  24. package/dist/commands/data_api.js +238 -260
  25. package/dist/commands/databases.js +67 -76
  26. package/dist/commands/deploy.js +31 -25
  27. package/dist/commands/dev.js +639 -719
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -355
  31. package/dist/commands/index.js +70 -65
  32. package/dist/commands/init.js +84 -119
  33. package/dist/commands/inspect.js +55 -55
  34. package/dist/commands/ip_allow.js +88 -120
  35. package/dist/commands/link.js +874 -1019
  36. package/dist/commands/logs.js +291 -0
  37. package/dist/commands/neon_auth.js +725 -933
  38. package/dist/commands/operations.js +34 -25
  39. package/dist/commands/orgs.js +28 -18
  40. package/dist/commands/profile.js +614 -845
  41. package/dist/commands/projects.js +313 -373
  42. package/dist/commands/psql.js +60 -58
  43. package/dist/commands/roles.js +55 -58
  44. package/dist/commands/schema_diff.js +87 -131
  45. package/dist/commands/set_context.js +34 -26
  46. package/dist/commands/snapshots.js +288 -413
  47. package/dist/commands/status.js +41 -37
  48. package/dist/commands/user.js +21 -10
  49. package/dist/commands/vpc_endpoints.js +85 -113
  50. package/dist/config.js +7 -6
  51. package/dist/config_format.js +50 -66
  52. package/dist/config_template.js +128 -157
  53. package/dist/context.js +183 -235
  54. package/dist/current_branch_fast_path.js +40 -49
  55. package/dist/dev/env.js +313 -394
  56. package/dist/dev/functions.js +54 -68
  57. package/dist/dev/inputs.js +46 -58
  58. package/dist/dev/runtime.js +135 -164
  59. package/dist/dev/websocket.js +766 -959
  60. package/dist/env.js +27 -33
  61. package/dist/env_file.js +118 -132
  62. package/dist/env_services.js +36 -38
  63. package/dist/errors.js +57 -68
  64. package/dist/functions_api.js +45 -43
  65. package/dist/help.js +189 -140
  66. package/dist/index.js +182 -257
  67. package/dist/init/agents.js +137 -118
  68. package/dist/init/auth.js +58 -68
  69. package/dist/init/bootstrap.js +325 -396
  70. package/dist/init/build_config.js +4 -2
  71. package/dist/init/detect_agent.js +56 -101
  72. package/dist/init/editors.js +35 -52
  73. package/dist/init/enrich_output.js +51 -66
  74. package/dist/init/extension.js +134 -171
  75. package/dist/init/inspect.js +179 -266
  76. package/dist/init/interactive.js +510 -622
  77. package/dist/init/neonctl.js +117 -168
  78. package/dist/init/orchestrate.js +157 -173
  79. package/dist/init/phases/auth.js +188 -202
  80. package/dist/init/phases/cleanup.js +23 -23
  81. package/dist/init/phases/db.js +251 -277
  82. package/dist/init/phases/getting_started.js +213 -223
  83. package/dist/init/phases/mcp.js +174 -224
  84. package/dist/init/phases/migrations.js +247 -248
  85. package/dist/init/phases/neon_auth.js +114 -133
  86. package/dist/init/phases/setup.js +546 -703
  87. package/dist/init/phases/skills.js +75 -86
  88. package/dist/init/phases/status.js +72 -67
  89. package/dist/init/resolve_context.js +102 -99
  90. package/dist/init/route_command.js +91 -98
  91. package/dist/init/skills.js +174 -218
  92. package/dist/init/vsix.js +77 -99
  93. package/dist/log.js +17 -16
  94. package/dist/neon_services.js +104 -129
  95. package/dist/parameters.gen.js +481 -471
  96. package/dist/pkg.js +17 -19
  97. package/dist/profile_keys.js +44 -47
  98. package/dist/psql/cli.js +44 -47
  99. package/dist/psql/command/cmd_cond.js +231 -406
  100. package/dist/psql/command/cmd_connect.js +557 -764
  101. package/dist/psql/command/cmd_copy.js +727 -983
  102. package/dist/psql/command/cmd_describe.js +1499 -1688
  103. package/dist/psql/command/cmd_format.js +733 -905
  104. package/dist/psql/command/cmd_io.js +1293 -2082
  105. package/dist/psql/command/cmd_lo.js +297 -359
  106. package/dist/psql/command/cmd_meta.js +727 -878
  107. package/dist/psql/command/cmd_misc.js +138 -172
  108. package/dist/psql/command/cmd_pipeline.js +547 -1099
  109. package/dist/psql/command/cmd_restrict.js +119 -155
  110. package/dist/psql/command/cmd_show.js +529 -688
  111. package/dist/psql/command/dispatch.js +261 -325
  112. package/dist/psql/command/inputQueue.js +35 -33
  113. package/dist/psql/command/shared.js +49 -63
  114. package/dist/psql/complete/filenames.js +90 -133
  115. package/dist/psql/complete/index.js +59 -97
  116. package/dist/psql/complete/matcher.js +236 -300
  117. package/dist/psql/complete/psqlVars.js +218 -223
  118. package/dist/psql/complete/queries.js +159 -177
  119. package/dist/psql/complete/rules.js +1493 -2299
  120. package/dist/psql/core/common.js +762 -1180
  121. package/dist/psql/core/help.js +456 -546
  122. package/dist/psql/core/mainloop.js +692 -1302
  123. package/dist/psql/core/prompt.js +391 -408
  124. package/dist/psql/core/settings.js +429 -644
  125. package/dist/psql/core/sqlHelp.js +480 -554
  126. package/dist/psql/core/startup.js +626 -815
  127. package/dist/psql/core/syncVars.js +67 -110
  128. package/dist/psql/core/variables.js +156 -278
  129. package/dist/psql/describe/formatters.js +884 -1285
  130. package/dist/psql/describe/processNamePattern.js +173 -260
  131. package/dist/psql/describe/queries.js +1368 -2403
  132. package/dist/psql/describe/versionGate.js +32 -41
  133. package/dist/psql/index.js +1414 -1927
  134. package/dist/psql/io/history.js +232 -271
  135. package/dist/psql/io/input.js +103 -108
  136. package/dist/psql/io/lineEditor/buffer.js +238 -319
  137. package/dist/psql/io/lineEditor/complete.js +135 -213
  138. package/dist/psql/io/lineEditor/filename.js +139 -148
  139. package/dist/psql/io/lineEditor/index.js +653 -870
  140. package/dist/psql/io/lineEditor/keymap.js +544 -702
  141. package/dist/psql/io/lineEditor/vt100.js +294 -341
  142. package/dist/psql/io/pgpass.js +158 -187
  143. package/dist/psql/io/pgservice.js +146 -183
  144. package/dist/psql/io/psqlrc.js +328 -403
  145. package/dist/psql/print/aligned.js +1020 -1683
  146. package/dist/psql/print/asciidoc.js +180 -214
  147. package/dist/psql/print/crosstab.js +281 -442
  148. package/dist/psql/print/csv.js +48 -70
  149. package/dist/psql/print/html.js +195 -226
  150. package/dist/psql/print/json.js +75 -88
  151. package/dist/psql/print/latex.js +291 -364
  152. package/dist/psql/print/pager.js +171 -242
  153. package/dist/psql/print/troff.js +194 -226
  154. package/dist/psql/print/unaligned.js +69 -95
  155. package/dist/psql/print/units.js +167 -169
  156. package/dist/psql/scanner/slash.js +428 -483
  157. package/dist/psql/scanner/sql.js +445 -889
  158. package/dist/psql/scanner/stringutils.js +309 -379
  159. package/dist/psql/types/index.js +2 -7
  160. package/dist/psql/types/scanner.js +25 -22
  161. package/dist/psql/wire/connection.js +2042 -2803
  162. package/dist/psql/wire/copy.js +84 -100
  163. package/dist/psql/wire/notify.js +39 -59
  164. package/dist/psql/wire/pipeline.js +305 -518
  165. package/dist/psql/wire/protocol.js +349 -417
  166. package/dist/psql/wire/sasl.js +180 -265
  167. package/dist/psql/wire/tls.js +400 -561
  168. package/dist/storage_api.js +115 -129
  169. package/dist/test_utils/fixtures.js +94 -113
  170. package/dist/test_utils/oauth_server.js +10 -7
  171. package/dist/test_utils/project_dir.js +33 -0
  172. package/dist/utils/ai_gateway_notice.js +131 -162
  173. package/dist/utils/api_enums.js +21 -28
  174. package/dist/utils/auth.js +10 -4
  175. package/dist/utils/branch_notice.js +20 -19
  176. package/dist/utils/branch_picker.js +83 -89
  177. package/dist/utils/cli_name.js +15 -12
  178. package/dist/utils/compute_units.js +20 -27
  179. package/dist/utils/config_diff.js +127 -158
  180. package/dist/utils/enrichers.js +95 -148
  181. package/dist/utils/esbuild.js +130 -189
  182. package/dist/utils/flags.js +35 -47
  183. package/dist/utils/formats.js +8 -15
  184. package/dist/utils/git_diff.js +69 -80
  185. package/dist/utils/inspect_db.js +101 -143
  186. package/dist/utils/inspect_queries.js +179 -142
  187. package/dist/utils/middlewares.js +37 -44
  188. package/dist/utils/openapi.js +87 -99
  189. package/dist/utils/package_manager.js +312 -110
  190. package/dist/utils/point_in_time.js +49 -53
  191. package/dist/utils/psql.js +89 -106
  192. package/dist/utils/service_picker.js +55 -58
  193. package/dist/utils/string.js +5 -5
  194. package/dist/utils/ui.js +38 -55
  195. package/dist/utils/write_sync.js +26 -35
  196. package/dist/utils/zip.js +4 -3
  197. package/dist/writer.js +67 -87
  198. package/package.json +7 -5
@@ -1,463 +1,302 @@
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
1
  import { alignedPrinter } from "./aligned.js";
2
+ //#region src/psql/print/crosstab.ts
38
3
  const DIGIT_RE = /^\d+$/;
39
4
  const SIGNED_DIGIT_RE = /^[+-]?\d+$/;
40
5
  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 };
6
+ let out = "";
7
+ let inquotes = false;
8
+ let hadQuotes = false;
9
+ let i = 0;
10
+ while (i < raw.length) {
11
+ const c = raw[i];
12
+ if (c === "\"") {
13
+ hadQuotes = true;
14
+ if (inquotes && raw[i + 1] === "\"") {
15
+ out += "\"";
16
+ i += 2;
17
+ continue;
18
+ }
19
+ inquotes = !inquotes;
20
+ i++;
21
+ continue;
22
+ }
23
+ out += inquotes ? c : c.toLowerCase();
24
+ i++;
25
+ }
26
+ return {
27
+ value: out,
28
+ hadQuotes
29
+ };
62
30
  };
63
31
  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 };
32
+ if (typeof arg === "number") {
33
+ if (!Number.isInteger(arg)) return {
34
+ ok: false,
35
+ error: `column number ${String(arg)} is not an integer`
36
+ };
37
+ const sign = arg < 0 ? -1 : 1;
38
+ const abs = Math.abs(arg);
39
+ if (abs < 1 || abs > fields.length) return {
40
+ ok: false,
41
+ error: `column number ${String(abs)} is out of range 1..${String(fields.length)}`
42
+ };
43
+ return {
44
+ ok: true,
45
+ index: abs - 1,
46
+ sign: allowSign ? sign : 1
47
+ };
48
+ }
49
+ let str = arg.trim();
50
+ let sign = 1;
51
+ if (allowSign && (str.startsWith("+") || str.startsWith("-"))) {
52
+ if (str.startsWith("-")) sign = -1;
53
+ const rest = str.slice(1);
54
+ if (rest.length > 0) str = rest;
55
+ }
56
+ if (str.length === 0) return {
57
+ ok: false,
58
+ error: "empty column reference"
59
+ };
60
+ if (DIGIT_RE.test(str)) {
61
+ const n = parseInt(str, 10);
62
+ if (n < 1 || n > fields.length) return {
63
+ ok: false,
64
+ error: `column number ${String(n)} is out of range 1..${String(fields.length)}`
65
+ };
66
+ return {
67
+ ok: true,
68
+ index: n - 1,
69
+ sign
70
+ };
71
+ }
72
+ const { value: needle } = dequoteDowncase(str);
73
+ let found = -1;
74
+ for (let i = 0; i < fields.length; i++) if (fields[i].name === needle) {
75
+ if (found >= 0) return {
76
+ ok: false,
77
+ error: `ambiguous column name: "${needle}"`
78
+ };
79
+ found = i;
80
+ }
81
+ if (found === -1) return {
82
+ ok: false,
83
+ error: `column name not found: "${needle}"`
84
+ };
85
+ return {
86
+ ok: true,
87
+ index: found,
88
+ sign
89
+ };
138
90
  };
139
- // ---------------------------------------------------------------------------
140
- // Cell value → comparable key + display string
141
- // ---------------------------------------------------------------------------
142
91
  /**
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
- */
92
+ * Build a stable string key for a header value so we can deduplicate
93
+ * vertical / horizontal headers consistently. We use `JSON.stringify`-ish
94
+ * encoding plus a leading discriminator so e.g. the string `"1"` and the
95
+ * number `1` don't collide (matches upstream where PQgetvalue returns a
96
+ * type-specific text form — the rows pre-serialised by the server are
97
+ * compared byte-for-byte).
98
+ *
99
+ * Null is represented by a sentinel so the printer can substitute
100
+ * `popt.nullPrint`.
101
+ */
153
102
  const NULL_SENTINEL = Symbol.for("neonctl.psql.crosstab.null");
154
103
  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)}`;
104
+ if (value === null || value === void 0) return NULL_SENTINEL;
105
+ if (typeof value === "string") return `s:${value}`;
106
+ if (typeof value === "number") return `n:${String(value)}`;
107
+ if (typeof value === "bigint") return `i:${String(value)}`;
108
+ if (typeof value === "boolean") return `b:${String(value)}`;
109
+ if (value instanceof Date) return `d:${value.toISOString()}`;
110
+ if (value instanceof Uint8Array) {
111
+ let hex = "x:";
112
+ for (const b of value) hex += b.toString(16).padStart(2, "0");
113
+ return hex;
114
+ }
115
+ return `j:${JSON.stringify(value)}`;
174
116
  };
175
117
  /**
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
- */
118
+ * Display form for a header value — used to populate the synthetic
119
+ * ResultSet's `fields[i].name` (which must be a string, not nullable).
120
+ * Mirrors upstream's `colname = piv_columns[…].name ? … : popt.nullPrint`.
121
+ */
180
122
  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);
123
+ if (value === null || value === void 0) return nullPrint;
124
+ if (typeof value === "string") return value;
125
+ if (typeof value === "number" || typeof value === "bigint") return value.toString();
126
+ if (typeof value === "boolean") return value ? "t" : "f";
127
+ if (value instanceof Date) return value.toISOString();
128
+ if (value instanceof Uint8Array) {
129
+ let hex = "\\x";
130
+ for (const b of value) hex += b.toString(16).padStart(2, "0");
131
+ return hex;
132
+ }
133
+ return JSON.stringify(value);
199
134
  };
200
135
  /**
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
- */
136
+ * Compare two cells under `+`/`-` numeric semantics: if both look like
137
+ * `/^-?\d+$/` (matching upstream's `rankSort` regex), compare as integers;
138
+ * otherwise compare as strings. Nulls sort last. The `sign` flag flips the
139
+ * order (descending).
140
+ */
206
141
  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;
142
+ const aNull = a === null || a === void 0;
143
+ const bNull = b === null || b === void 0;
144
+ if (aNull && bNull) return 0;
145
+ if (aNull) return 1;
146
+ if (bNull) return -1;
147
+ const aStr = typeof a === "string" ? a : headerDisplay(a, "");
148
+ const bStr = typeof b === "string" ? b : headerDisplay(b, "");
149
+ if (SIGNED_DIGIT_RE.test(aStr) && SIGNED_DIGIT_RE.test(bStr)) {
150
+ const an = parseInt(aStr, 10);
151
+ const bn = parseInt(bStr, 10);
152
+ if (an < bn) return -1 * sign;
153
+ if (an > bn) return 1 * sign;
154
+ return 0;
155
+ }
156
+ if (aStr < bStr) return -1 * sign;
157
+ if (aStr > bStr) return 1 * sign;
158
+ return 0;
231
159
  };
232
160
  /**
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 };
161
+ * Pivot a ResultSet into a `{ rowHeaders, colHeaders, matrix }` shape,
162
+ * returning a synthetic ResultSet ready for the aligned printer.
163
+ *
164
+ * Detailed algorithm:
165
+ *
166
+ * 1. Resolve `colV` / `colH` / `colD` / `sortColH` to zero-based field
167
+ * indices via {@link indexOfColumn}. `colV` defaults to 0, `colH` to 1.
168
+ * `colD` defaults to the only remaining column when there are exactly
169
+ * three; otherwise we require it explicitly (matching upstream).
170
+ * 2. Walk rows once. For each row:
171
+ * - `vKey` = header-key of the row's colV cell;
172
+ * - `hKey` = header-key of the row's colH cell;
173
+ * - If we haven't seen `vKey` or `hKey` before, give them the next
174
+ * rank in first-appearance order, and capture the sort value
175
+ * (from colSort) for hKey.
176
+ * - Stash the cell at `(vRank, hRank)`. If a value already lives
177
+ * there, surface a duplicate-pair error.
178
+ * 3. If `sortColH` was supplied, stable-sort the horizontal header entries
179
+ * by their captured sort value (numeric if both look numeric, else
180
+ * string; `sign` for ascending/descending) and reassign ranks.
181
+ * 4. Construct the synthetic ResultSet: one field for colV (carries colV's
182
+ * original FieldDescription) + one field per horizontal header value
183
+ * (each carrying colD's FieldDescription, so the aligned printer's
184
+ * right-align heuristic kicks in for numeric data).
185
+ * 5. Rows: for each vertical header in rank order, emit `[vHeaderValue,
186
+ * ...cellsByHorizontalRank]`. Unfilled cells are `""` so the printer
187
+ * just emits empty padding (matching upstream's
188
+ * "non-initialized cells must be set to an empty string" pass).
189
+ */
190
+ const pivotResultSet = (rs, opts, nullPrint = "") => {
191
+ if (rs.fields.length < 3) return { error: "query must return at least three columns" };
192
+ const colV = opts.colV ?? 1;
193
+ const colH = opts.colH ?? 2;
194
+ const vRes = indexOfColumn(colV, rs.fields, false);
195
+ if (!vRes.ok) return { error: vRes.error };
196
+ const hRes = indexOfColumn(colH, rs.fields, false);
197
+ if (!hRes.ok) return { error: hRes.error };
198
+ if (vRes.index === hRes.index) return { error: "vertical and horizontal headers must be different columns" };
199
+ let dataIdx;
200
+ if (opts.colD === void 0) {
201
+ let candidate = -1;
202
+ for (let i = 0; i < rs.fields.length; i++) if (i !== vRes.index && i !== hRes.index) {
203
+ candidate = i;
204
+ break;
205
+ }
206
+ if (candidate < 0) return { error: "no data column available" };
207
+ dataIdx = candidate;
208
+ } else {
209
+ const dRes = indexOfColumn(opts.colD, rs.fields, false);
210
+ if (!dRes.ok) return { error: dRes.error };
211
+ dataIdx = dRes.index;
212
+ }
213
+ let sortIdx = -1;
214
+ let sortSign = 1;
215
+ if (opts.sortColH !== void 0) {
216
+ const sRes = indexOfColumn(opts.sortColH, rs.fields, true);
217
+ if (!sRes.ok) return { error: sRes.error };
218
+ sortIdx = sRes.index;
219
+ sortSign = sRes.sign;
220
+ }
221
+ const vHeaders = /* @__PURE__ */ new Map();
222
+ const hHeaders = /* @__PURE__ */ new Map();
223
+ const matrix = /* @__PURE__ */ new Map();
224
+ for (const row of rs.rows) {
225
+ const vVal = row[vRes.index];
226
+ const hVal = row[hRes.index];
227
+ const dVal = row[dataIdx];
228
+ const vk = headerKey(vVal);
229
+ const hk = headerKey(hVal);
230
+ let vEntry = vHeaders.get(vk);
231
+ if (!vEntry) {
232
+ vEntry = {
233
+ key: vk,
234
+ value: vVal,
235
+ rank: vHeaders.size,
236
+ sortValue: null
237
+ };
238
+ vHeaders.set(vk, vEntry);
239
+ }
240
+ let hEntry = hHeaders.get(hk);
241
+ if (!hEntry) {
242
+ if (hHeaders.size >= 1600) return { error: "maximum number of columns (1600) exceeded" };
243
+ hEntry = {
244
+ key: hk,
245
+ value: hVal,
246
+ rank: hHeaders.size,
247
+ sortValue: sortIdx >= 0 ? row[sortIdx] : null
248
+ };
249
+ hHeaders.set(hk, hEntry);
250
+ }
251
+ const cellKey = `${String(vEntry.rank)}|${String(hEntry.rank)}`;
252
+ if (matrix.has(cellKey)) return { error: `query result contains multiple data values for row "${headerDisplay(vEntry.value, "(null)")}", column "${headerDisplay(hEntry.value, "(null)")}"` };
253
+ matrix.set(cellKey, dVal);
254
+ }
255
+ const hArr = Array.from(hHeaders.values());
256
+ if (sortIdx >= 0) hArr.sort((a, b) => {
257
+ const c = cmpForSort(a.sortValue, b.sortValue, sortSign);
258
+ if (c !== 0) return c;
259
+ return a.rank - b.rank;
260
+ });
261
+ const vArr = Array.from(vHeaders.values()).sort((a, b) => a.rank - b.rank);
262
+ const newFields = [{ ...rs.fields[vRes.index] }, ...hArr.map((h) => ({
263
+ ...rs.fields[dataIdx],
264
+ name: headerDisplay(h.value, nullPrint)
265
+ }))];
266
+ const newRows = vArr.map((v) => {
267
+ const row = new Array(hArr.length + 1);
268
+ row[0] = v.value;
269
+ for (let i = 0; i < hArr.length; i++) {
270
+ const key = `${String(v.rank)}|${String(hArr[i].rank)}`;
271
+ row[i + 1] = matrix.has(key) ? matrix.get(key) : "";
272
+ }
273
+ return row;
274
+ });
275
+ return { rs: {
276
+ command: rs.command,
277
+ rowCount: vArr.length,
278
+ oid: null,
279
+ fields: newFields,
280
+ rows: newRows,
281
+ notices: []
282
+ } };
436
283
  };
437
- // ---------------------------------------------------------------------------
438
- // printCrosstab — pivot + delegate to alignedPrinter
439
- // ---------------------------------------------------------------------------
440
284
  /**
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;
285
+ * High-level entry: pivot `rs` per `opts`, then forward the synthetic
286
+ * ResultSet to `alignedPrinter.printQuery`. Returns a `CrosstabError` if
287
+ * pivoting fails; on success returns `undefined` (matching the rest of the
288
+ * `print/` API surface).
289
+ *
290
+ * We deliberately use the aligned printer regardless of `printOpts.topt.format`:
291
+ * upstream psql's `\crosstabview` always renders through the table printer
292
+ * (it ignores `\pset format` for the duration of the call), and our tests
293
+ * lean on the aligned printer's borders / nullPrint / numericLocale handling
294
+ * matching exactly.
295
+ */
296
+ const printCrosstab = async (rs, opts, printOpts, out) => {
297
+ const result = pivotResultSet(rs, opts, printOpts.topt.nullPrint);
298
+ if ("error" in result) return result;
299
+ await alignedPrinter.printQuery(result.rs, printOpts, out);
463
300
  };
301
+ //#endregion
302
+ export { pivotResultSet, printCrosstab };