neon 2.47.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.
- package/README.md +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +395 -0
- package/dist/_shared/env-core/reuse-secrets.js +179 -0
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +575 -658
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +642 -681
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -341
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -64
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +133 -147
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +9 -7
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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 };
|