neon 3.0.0 → 3.1.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 (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -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 };