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.
- package/README.md +70 -5
- package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/_chunks/credentials-MYdHdKah.js +188 -0
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
- package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
- package/dist/_chunks/profiles-Ir29rqns.js +217 -0
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -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 +573 -690
- 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 +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +255 -305
- package/dist/commands/functions.js +275 -355
- 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 +615 -846
- 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 +2 -446
- package/dist/dev/functions.js +54 -68
- 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 +2 -51
- 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 +728 -984
- 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 +2 -2193
- 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 +2 -1148
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +260 -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 +2 -1253
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1303
- 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 +2 -846
- 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 +2 -2030
- 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 +8 -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 +130 -189
- 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 +39 -45
- 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 +11 -6
- package/dist/_shared/auth_selection.js +0 -86
- package/dist/_shared/credentials.js +0 -209
- package/dist/_shared/env-core/env.js +0 -558
- package/dist/_shared/env-core/reuse-secrets.js +0 -223
- package/dist/_shared/paths.js +0 -148
- package/dist/_shared/profiles.js +0 -276
- package/dist/_shared/secure_file.js +0 -43
|
@@ -1,766 +1,607 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `\sf [+] FUNCNAME` / `\sv [+] VIEWNAME` show-source commands and their
|
|
3
|
-
* `\ef` / `\ev` edit-form siblings, ported from upstream `command.c`'s
|
|
4
|
-
* `exec_command_sf_sv` and `exec_command_ef_ev`.
|
|
5
|
-
*
|
|
6
|
-
* Behaviour matches upstream byte-for-byte for the show forms:
|
|
7
|
-
*
|
|
8
|
-
* 1. Lookup the object's OID — function via `regproc` (or `regprocedure`
|
|
9
|
-
* when the name carries an argument list `(int)`), view via `regclass`.
|
|
10
|
-
* 2. Fetch the definition — `pg_get_functiondef(oid)` for functions; for
|
|
11
|
-
* views we re-assemble the `CREATE OR REPLACE VIEW … AS …` head (with
|
|
12
|
-
* schema-qualified name, reloptions, and optional `WITH CHECK OPTION`)
|
|
13
|
-
* around the body returned by `pg_get_viewdef(oid, true)`.
|
|
14
|
-
* 3. Stream the rendered text to `stdout`, optionally prefixed by line
|
|
15
|
-
* numbers when the user passed `+` (e.g. `\sf+ foo`).
|
|
16
|
-
*
|
|
17
|
-
* Line-number formatting (mirrors upstream `print_with_linenumbers`):
|
|
18
|
-
*
|
|
19
|
-
* - For functions: lines before the body marker (`AS `, `BEGIN `, or
|
|
20
|
-
* `RETURN `) are unnumbered and rendered as ` <line>\n` (8 spaces
|
|
21
|
-
* of padding). Body lines render as `<lineno> <line>\n` where the
|
|
22
|
-
* numeric field is left-justified in a 7-character slot, with one
|
|
23
|
-
* literal space separator. The first body line becomes line 1.
|
|
24
|
-
* - For views: every line is a "body" line — `lineno` starts at 1 and
|
|
25
|
-
* increments for every output line, no header padding.
|
|
26
|
-
*
|
|
27
|
-
* Edit forms (`\ef` / `\ev`):
|
|
28
|
-
* We do not implement editor invocation — that needs TTY interaction and
|
|
29
|
-
* `$EDITOR` semantics outside the scope of this embedded psql. When the
|
|
30
|
-
* user supplies a name we route through the same fetch+print path as the
|
|
31
|
-
* show forms (`\ef foo` ≡ `\sf foo`); without a name we error with a hint
|
|
32
|
-
* pointing back at `\sf` / `\sv`.
|
|
33
|
-
*
|
|
34
|
-
* Argument parsing matches upstream's `OT_WHOLE_LINE`: we slurp the rest of
|
|
35
|
-
* the line and trim, so `\sf myschema.foo ` round-trips cleanly without
|
|
36
|
-
* splitting on the dot or whitespace inside parens.
|
|
37
|
-
*/
|
|
38
1
|
import { writeErr, writeOut } from "./shared.js";
|
|
39
|
-
|
|
40
|
-
// Helpers
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
2
|
+
//#region src/psql/command/cmd_show.ts
|
|
42
3
|
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
4
|
+
* Emit a `\<cmd>: <message>` error to stderr, stash the message on
|
|
5
|
+
* `lastErrorResult` so `\errverbose` / the mainloop fallback see it, and
|
|
6
|
+
* return an error result with `errorWritten: true` so the mainloop doesn't
|
|
7
|
+
* double-print.
|
|
8
|
+
*/
|
|
48
9
|
const errResult = (ctx, message) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
10
|
+
ctx.settings.lastErrorResult = { message };
|
|
11
|
+
writeErr(`\\${ctx.cmdName}: ${message}\n`);
|
|
12
|
+
return {
|
|
13
|
+
status: "error",
|
|
14
|
+
errorWritten: true
|
|
15
|
+
};
|
|
52
16
|
};
|
|
53
17
|
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
18
|
+
* Format a server error for stderr the way upstream's
|
|
19
|
+
* `minimal_error_message` does: `<severity>: <primary message>\n`. Falls
|
|
20
|
+
* back to "ERROR:" + Error.message when the error doesn't carry severity /
|
|
21
|
+
* message fields (e.g. a wire-layer rejection).
|
|
22
|
+
*/
|
|
59
23
|
const formatServerError = (err) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
if (err instanceof Error)
|
|
68
|
-
return `ERROR: ${err.message}`;
|
|
69
|
-
return `ERROR: ${safeToString(err)}`;
|
|
24
|
+
if (err && typeof err === "object") {
|
|
25
|
+
const e = err;
|
|
26
|
+
return `${e.severity ?? "ERROR"}: ${e.message ?? (err instanceof Error ? err.message : safeToString(err))}`;
|
|
27
|
+
}
|
|
28
|
+
if (err instanceof Error) return `ERROR: ${err.message}`;
|
|
29
|
+
return `ERROR: ${safeToString(err)}`;
|
|
70
30
|
};
|
|
71
31
|
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
32
|
+
* Coerce an unknown value to a string defensively. Plain non-`Error`
|
|
33
|
+
* objects would render as `[object Object]` via the default `String()`
|
|
34
|
+
* path; we sidestep that by JSON-encoding when possible (falls back to
|
|
35
|
+
* the typeof when JSON throws — e.g. circular structures).
|
|
36
|
+
*/
|
|
77
37
|
const safeToString = (v) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return String(v);
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
return JSON.stringify(v) ?? typeof v;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return typeof v;
|
|
94
|
-
}
|
|
38
|
+
if (v === null) return "null";
|
|
39
|
+
if (v === void 0) return "undefined";
|
|
40
|
+
if (typeof v === "string") return v;
|
|
41
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(v) ?? typeof v;
|
|
44
|
+
} catch {
|
|
45
|
+
return typeof v;
|
|
46
|
+
}
|
|
95
47
|
};
|
|
96
48
|
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
49
|
+
* Print a server-side query failure the way upstream does — `<sev>: <msg>`
|
|
50
|
+
* directly on stderr, without the `\<cmd>: ` prefix that local-only errors
|
|
51
|
+
* use. Also stashes the message on `lastErrorResult` so `\errverbose`
|
|
52
|
+
* survives.
|
|
53
|
+
*/
|
|
102
54
|
const queryErrResult = (ctx, err) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
? err.message
|
|
111
|
-
: safeToString(err),
|
|
112
|
-
};
|
|
113
|
-
writeErr(`${line}\n`);
|
|
114
|
-
return { status: "error", errorWritten: true };
|
|
55
|
+
const line = formatServerError(err);
|
|
56
|
+
ctx.settings.lastErrorResult = { message: err && typeof err === "object" && err.message ? err.message : err instanceof Error ? err.message : safeToString(err) };
|
|
57
|
+
writeErr(`${line}\n`);
|
|
58
|
+
return {
|
|
59
|
+
status: "error",
|
|
60
|
+
errorWritten: true
|
|
61
|
+
};
|
|
115
62
|
};
|
|
116
63
|
const conn = (ctx) => ctx.settings.db;
|
|
117
64
|
const noConn = (ctx) => errResult(ctx, "no connection to the server");
|
|
118
65
|
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
66
|
+
* Read the object descriptor as a whole-line argument with surrounding
|
|
67
|
+
* whitespace AND trailing semicolons stripped. Returns `null` when no
|
|
68
|
+
* name was supplied (after the strip — i.e. `\sf ` or `\sf ;;` is
|
|
69
|
+
* treated as empty).
|
|
70
|
+
*
|
|
71
|
+
* Upstream `psql_scan_slash_option(scan_state, OT_WHOLE_LINE, …)` keeps
|
|
72
|
+
* the `;` in the returned string, but `exec_command_sf_sv` (and its `ef`/
|
|
73
|
+
* `ev` siblings) trim trailing whitespace + `;` before passing the
|
|
74
|
+
* descriptor to `lookup_object_oid`. Without this, `\sf ts_debug(text);`
|
|
75
|
+
* sends `'ts_debug(text);'::regprocedure` to the server, which the
|
|
76
|
+
* regprocedure input parser rejects with "expected a right parenthesis".
|
|
77
|
+
*/
|
|
131
78
|
const readObjDesc = (ctx) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// `\sf foo(arg) ;; ` round-trips like vanilla psql.
|
|
135
|
-
const trimmed = raw.replace(/[\s;]+$/, "").trimStart();
|
|
136
|
-
return trimmed.length === 0 ? null : trimmed;
|
|
79
|
+
const trimmed = ctx.restOfLine().replace(/[\s;]+$/, "").trimStart();
|
|
80
|
+
return trimmed.length === 0 ? null : trimmed;
|
|
137
81
|
};
|
|
138
82
|
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
83
|
+
* Decode whether the command name ends in `+` (request line numbers). The
|
|
84
|
+
* caller knows the base name (`sf`, `sv`, `ef`, `ev`); any extra letters
|
|
85
|
+
* are looked at for a literal `+`.
|
|
86
|
+
*/
|
|
143
87
|
const decodeShowSuffix = (cmdName, base) => {
|
|
144
|
-
|
|
145
|
-
return { plus: tail.includes("+") };
|
|
88
|
+
return { plus: cmdName.slice(base.length).includes("+") };
|
|
146
89
|
};
|
|
147
90
|
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
91
|
+
* Look up a function OID from `desc`. Mirrors upstream's
|
|
92
|
+
* `lookup_object_oid(EditableFunction, ...)` exactly:
|
|
93
|
+
*
|
|
94
|
+
* - If `desc` contains `(`, cast through `regprocedure` (which resolves
|
|
95
|
+
* by full argument signature, e.g. `foo(int)`).
|
|
96
|
+
* - Otherwise cast through `regproc` (which matches by bare name and
|
|
97
|
+
* errors on overloaded ambiguity).
|
|
98
|
+
*
|
|
99
|
+
* The descriptor is passed as a SQL string literal — we use the server's
|
|
100
|
+
* `escapeLiteral` to mirror libpq's `appendStringLiteralConn`.
|
|
101
|
+
*/
|
|
159
102
|
const lookupFunctionOid = async (c, desc) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
103
|
+
const cast = desc.includes("(") ? "regprocedure" : "regproc";
|
|
104
|
+
const sql = `SELECT ${c.escapeLiteral(desc)}::pg_catalog.${cast}::pg_catalog.oid`;
|
|
105
|
+
try {
|
|
106
|
+
const rs = await c.query(sql, []);
|
|
107
|
+
if (rs.rows.length !== 1 || rs.rows[0][0] === null) return {
|
|
108
|
+
ok: false,
|
|
109
|
+
err: /* @__PURE__ */ new Error("object lookup returned no rows")
|
|
110
|
+
};
|
|
111
|
+
const raw = cellToString(rs.rows[0][0]);
|
|
112
|
+
const oid = Number(raw);
|
|
113
|
+
if (!Number.isFinite(oid)) return {
|
|
114
|
+
ok: false,
|
|
115
|
+
err: /* @__PURE__ */ new Error(`invalid oid in lookup result: ${raw}`)
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
oid
|
|
120
|
+
};
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
err
|
|
125
|
+
};
|
|
126
|
+
}
|
|
183
127
|
};
|
|
184
128
|
/**
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
129
|
+
* Look up a view OID from `desc` via `regclass`. Matches upstream's
|
|
130
|
+
* `lookup_object_oid(EditableView, ...)`. Note that this does NOT verify
|
|
131
|
+
* the relation is actually a view; the kind check happens in
|
|
132
|
+
* `getViewCreateCmd` where upstream catches it via the relkind column.
|
|
133
|
+
*/
|
|
190
134
|
const lookupRelationOid = async (c, desc) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
135
|
+
const sql = `SELECT ${c.escapeLiteral(desc)}::pg_catalog.regclass::pg_catalog.oid`;
|
|
136
|
+
try {
|
|
137
|
+
const rs = await c.query(sql, []);
|
|
138
|
+
if (rs.rows.length !== 1 || rs.rows[0][0] === null) return {
|
|
139
|
+
ok: false,
|
|
140
|
+
err: /* @__PURE__ */ new Error("object lookup returned no rows")
|
|
141
|
+
};
|
|
142
|
+
const raw = cellToString(rs.rows[0][0]);
|
|
143
|
+
const oid = Number(raw);
|
|
144
|
+
if (!Number.isFinite(oid)) return {
|
|
145
|
+
ok: false,
|
|
146
|
+
err: /* @__PURE__ */ new Error(`invalid oid in lookup result: ${raw}`)
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
oid
|
|
151
|
+
};
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
err
|
|
156
|
+
};
|
|
157
|
+
}
|
|
213
158
|
};
|
|
214
159
|
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
160
|
+
* Coerce a wire-layer cell to a string. Text-mode results arrive as
|
|
161
|
+
* strings; null is treated as "" so missing-row paths fall through to
|
|
162
|
+
* empty output instead of crashing.
|
|
163
|
+
*/
|
|
219
164
|
const cellToString = (v) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return String(v);
|
|
230
|
-
}
|
|
231
|
-
// Non-primitive fallback: encode JSON so we never emit a stray
|
|
232
|
-
// `[object Object]`. The wire layer hands us strings or nulls in
|
|
233
|
-
// practice, so this branch is defensive only.
|
|
234
|
-
try {
|
|
235
|
-
return JSON.stringify(v) ?? "";
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
return "";
|
|
239
|
-
}
|
|
165
|
+
if (v === null || v === void 0) return "";
|
|
166
|
+
if (typeof v === "string") return v;
|
|
167
|
+
if (Buffer.isBuffer(v)) return v.toString("utf-8");
|
|
168
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
169
|
+
try {
|
|
170
|
+
return JSON.stringify(v) ?? "";
|
|
171
|
+
} catch {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
240
174
|
};
|
|
241
175
|
/**
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
176
|
+
* Fetch the CREATE FUNCTION source for `oid` via
|
|
177
|
+
* `pg_catalog.pg_get_functiondef(oid)`. Upstream guarantees the result is
|
|
178
|
+
* newline-terminated; we re-assert that here so the caller can stream
|
|
179
|
+
* straight to stdout (or hand to the line-number formatter).
|
|
180
|
+
*/
|
|
247
181
|
const getFunctionCreateCmd = async (c, oid) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
182
|
+
const sql = `SELECT pg_catalog.pg_get_functiondef(${oid})`;
|
|
183
|
+
try {
|
|
184
|
+
const rs = await c.query(sql, []);
|
|
185
|
+
if (rs.rows.length !== 1) return {
|
|
186
|
+
ok: false,
|
|
187
|
+
err: /* @__PURE__ */ new Error("function definition not found")
|
|
188
|
+
};
|
|
189
|
+
let def = cellToString(rs.rows[0][0]);
|
|
190
|
+
if (def.length > 0 && !def.endsWith("\n")) def += "\n";
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
def
|
|
194
|
+
};
|
|
195
|
+
} catch (err) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
err
|
|
199
|
+
};
|
|
200
|
+
}
|
|
265
201
|
};
|
|
266
202
|
/**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
203
|
+
* Quote an SQL identifier when needed. Mirrors libpq's `fmtId`: lowercase
|
|
204
|
+
* ASCII identifiers starting with `[a-z_]` and continuing with
|
|
205
|
+
* `[a-z0-9_$]` may go unquoted; anything else gets double-quoted with
|
|
206
|
+
* embedded double-quotes doubled. Used to schema-qualify view names in
|
|
207
|
+
* the synthesised `CREATE OR REPLACE VIEW …` head.
|
|
208
|
+
*/
|
|
273
209
|
const fmtId = (ident) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
return `"${ident.replace(/"/g, '""')}"`;
|
|
210
|
+
if (/^[a-z_][a-z0-9_$]*$/.test(ident) && !RESERVED_WORDS.has(ident)) return ident;
|
|
211
|
+
return `"${ident.replace(/"/g, "\"\"")}"`;
|
|
278
212
|
};
|
|
279
213
|
/**
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const RESERVED_WORDS = new Set([
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
214
|
+
* Minimal reserved-word set used by `fmtId`. Upstream's `fmtId` is much
|
|
215
|
+
* more conservative — any keyword needs quoting regardless of category.
|
|
216
|
+
* For our use case we only quote the keywords that show up as actual
|
|
217
|
+
* relation names; that's vanishingly rare in practice (CREATE VIEW
|
|
218
|
+
* "select" AS … is legal but no one does it). Keeping the set small
|
|
219
|
+
* avoids a large keyword table; the worst case is an un-needed
|
|
220
|
+
* double-quote pair, which is still valid SQL.
|
|
221
|
+
*/
|
|
222
|
+
const RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
223
|
+
"all",
|
|
224
|
+
"analyse",
|
|
225
|
+
"analyze",
|
|
226
|
+
"and",
|
|
227
|
+
"any",
|
|
228
|
+
"array",
|
|
229
|
+
"as",
|
|
230
|
+
"asc",
|
|
231
|
+
"asymmetric",
|
|
232
|
+
"both",
|
|
233
|
+
"case",
|
|
234
|
+
"cast",
|
|
235
|
+
"check",
|
|
236
|
+
"collate",
|
|
237
|
+
"column",
|
|
238
|
+
"constraint",
|
|
239
|
+
"create",
|
|
240
|
+
"current_catalog",
|
|
241
|
+
"current_date",
|
|
242
|
+
"current_role",
|
|
243
|
+
"current_time",
|
|
244
|
+
"current_timestamp",
|
|
245
|
+
"current_user",
|
|
246
|
+
"default",
|
|
247
|
+
"deferrable",
|
|
248
|
+
"desc",
|
|
249
|
+
"distinct",
|
|
250
|
+
"do",
|
|
251
|
+
"else",
|
|
252
|
+
"end",
|
|
253
|
+
"except",
|
|
254
|
+
"false",
|
|
255
|
+
"fetch",
|
|
256
|
+
"for",
|
|
257
|
+
"foreign",
|
|
258
|
+
"from",
|
|
259
|
+
"grant",
|
|
260
|
+
"group",
|
|
261
|
+
"having",
|
|
262
|
+
"in",
|
|
263
|
+
"initially",
|
|
264
|
+
"intersect",
|
|
265
|
+
"into",
|
|
266
|
+
"lateral",
|
|
267
|
+
"leading",
|
|
268
|
+
"limit",
|
|
269
|
+
"localtime",
|
|
270
|
+
"localtimestamp",
|
|
271
|
+
"not",
|
|
272
|
+
"null",
|
|
273
|
+
"offset",
|
|
274
|
+
"on",
|
|
275
|
+
"only",
|
|
276
|
+
"or",
|
|
277
|
+
"order",
|
|
278
|
+
"placing",
|
|
279
|
+
"primary",
|
|
280
|
+
"references",
|
|
281
|
+
"returning",
|
|
282
|
+
"select",
|
|
283
|
+
"session_user",
|
|
284
|
+
"some",
|
|
285
|
+
"symmetric",
|
|
286
|
+
"table",
|
|
287
|
+
"then",
|
|
288
|
+
"to",
|
|
289
|
+
"trailing",
|
|
290
|
+
"true",
|
|
291
|
+
"union",
|
|
292
|
+
"unique",
|
|
293
|
+
"user",
|
|
294
|
+
"using",
|
|
295
|
+
"variadic",
|
|
296
|
+
"when",
|
|
297
|
+
"where",
|
|
298
|
+
"window",
|
|
299
|
+
"with"
|
|
366
300
|
]);
|
|
367
301
|
/**
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
302
|
+
* Re-build a `CREATE OR REPLACE VIEW <schema>.<name>[ WITH (opts)] AS
|
|
303
|
+
* <body>[\n WITH <checkoption> CHECK OPTION]\n` definition the same way
|
|
304
|
+
* upstream's `get_create_object_cmd(EditableView)` does. Returns either
|
|
305
|
+
* the assembled text or a synthetic error when the relation isn't
|
|
306
|
+
* actually a view.
|
|
307
|
+
*/
|
|
374
308
|
const getViewCreateCmd = async (c, oid) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
out += `${fmtId(nspname)}.${fmtId(relname)}`;
|
|
417
|
-
// reloptions: postgres returns it as a text-mode array literal like
|
|
418
|
-
// `{foo=bar,baz=qux}`; we only need to detect non-empty (different
|
|
419
|
-
// from the literal `{}`) and split entries on `,` outside quotes.
|
|
420
|
-
const reloptStr = reloptions === null ? null : cellToString(reloptions);
|
|
421
|
-
if (reloptStr !== null && reloptStr.length > 2) {
|
|
422
|
-
out += "\n WITH (";
|
|
423
|
-
out += renderReloptions(reloptStr);
|
|
424
|
-
out += ")";
|
|
425
|
-
}
|
|
426
|
-
out += ` AS\n${viewdef}`;
|
|
427
|
-
// Strip trailing semicolon from pg_get_viewdef.
|
|
428
|
-
if (out.endsWith(";")) {
|
|
429
|
-
out = out.slice(0, -1);
|
|
430
|
-
}
|
|
431
|
-
if (checkoption !== "") {
|
|
432
|
-
out += `\n WITH ${checkoption} CHECK OPTION`;
|
|
433
|
-
}
|
|
434
|
-
if (!out.endsWith("\n"))
|
|
435
|
-
out += "\n";
|
|
436
|
-
return { ok: true, def: out };
|
|
309
|
+
const sql = (c.serverVersion >= 90400 ? "modern" : "legacy") === "modern" ? `SELECT nspname, relname, relkind, pg_catalog.pg_get_viewdef(c.oid, true), pg_catalog.array_remove(pg_catalog.array_remove(c.reloptions,'check_option=local'),'check_option=cascaded') AS reloptions, CASE WHEN 'check_option=local' = ANY (c.reloptions) THEN 'LOCAL'::text WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid WHERE c.oid = ${oid}` : `SELECT nspname, relname, relkind, pg_catalog.pg_get_viewdef(c.oid, true), c.reloptions AS reloptions, NULL AS checkoption FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid WHERE c.oid = ${oid}`;
|
|
310
|
+
let rs;
|
|
311
|
+
try {
|
|
312
|
+
rs = await c.query(sql, []);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
err
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (rs.rows.length !== 1) return {
|
|
320
|
+
ok: false,
|
|
321
|
+
err: /* @__PURE__ */ new Error("view definition not found")
|
|
322
|
+
};
|
|
323
|
+
const row = rs.rows[0];
|
|
324
|
+
const nspname = cellToString(row[0]);
|
|
325
|
+
const relname = cellToString(row[1]);
|
|
326
|
+
const relkind = cellToString(row[2]);
|
|
327
|
+
const viewdef = cellToString(row[3]);
|
|
328
|
+
const reloptions = row[4];
|
|
329
|
+
const checkoption = cellToString(row[5]);
|
|
330
|
+
if (relkind !== "v") return {
|
|
331
|
+
ok: false,
|
|
332
|
+
err: /* @__PURE__ */ new Error(`"${nspname}.${relname}" is not a view`)
|
|
333
|
+
};
|
|
334
|
+
let out = "CREATE OR REPLACE VIEW ";
|
|
335
|
+
out += `${fmtId(nspname)}.${fmtId(relname)}`;
|
|
336
|
+
const reloptStr = reloptions === null ? null : cellToString(reloptions);
|
|
337
|
+
if (reloptStr !== null && reloptStr.length > 2) {
|
|
338
|
+
out += "\n WITH (";
|
|
339
|
+
out += renderReloptions(reloptStr);
|
|
340
|
+
out += ")";
|
|
341
|
+
}
|
|
342
|
+
out += ` AS\n${viewdef}`;
|
|
343
|
+
if (out.endsWith(";")) out = out.slice(0, -1);
|
|
344
|
+
if (checkoption !== "") out += `\n WITH ${checkoption} CHECK OPTION`;
|
|
345
|
+
if (!out.endsWith("\n")) out += "\n";
|
|
346
|
+
return {
|
|
347
|
+
ok: true,
|
|
348
|
+
def: out
|
|
349
|
+
};
|
|
437
350
|
};
|
|
438
351
|
/**
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
352
|
+
* Render a Postgres text-mode array literal of `key=value` reloption
|
|
353
|
+
* entries (e.g. `{security_barrier=true,security_invoker=false}`) into
|
|
354
|
+
* the comma-separated `key=value, key2=value2` form upstream emits
|
|
355
|
+
* inside the `WITH (…)` clause.
|
|
356
|
+
*
|
|
357
|
+
* Mirrors `appendReloptionsArray`'s output behaviour for the limited
|
|
358
|
+
* surface relevant to views (no per-namespace options, no embedded
|
|
359
|
+
* quotes). For any value that contains characters that would need
|
|
360
|
+
* escaping in SQL — anything other than `[A-Za-z0-9_.\-]` — we render
|
|
361
|
+
* it as a quoted string literal, matching upstream's `appendStringLiteral`
|
|
362
|
+
* fallback.
|
|
363
|
+
*/
|
|
451
364
|
const renderReloptions = (literal) => {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
.replace(/\\"/g, '"')
|
|
471
|
-
.replace(/\\\\/g, "\\");
|
|
472
|
-
}
|
|
473
|
-
const eq = entry.indexOf("=");
|
|
474
|
-
if (eq < 0) {
|
|
475
|
-
out.push(entry);
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
const key = entry.slice(0, eq);
|
|
479
|
-
const value = entry.slice(eq + 1);
|
|
480
|
-
if (/^[A-Za-z0-9_.-]+$/.test(value)) {
|
|
481
|
-
out.push(`${key}=${value}`);
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
// Quote the value as a SQL string literal.
|
|
485
|
-
out.push(`${key}='${value.replace(/'/g, "''")}'`);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return out.join(", ");
|
|
365
|
+
if (!literal.startsWith("{") || !literal.endsWith("}")) return literal;
|
|
366
|
+
const inside = literal.slice(1, -1);
|
|
367
|
+
if (inside.length === 0) return "";
|
|
368
|
+
const entries = splitArrayElems(inside);
|
|
369
|
+
const out = [];
|
|
370
|
+
for (let entry of entries) {
|
|
371
|
+
if (entry.startsWith("\"") && entry.endsWith("\"")) entry = entry.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
372
|
+
const eq = entry.indexOf("=");
|
|
373
|
+
if (eq < 0) {
|
|
374
|
+
out.push(entry);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const key = entry.slice(0, eq);
|
|
378
|
+
const value = entry.slice(eq + 1);
|
|
379
|
+
if (/^[A-Za-z0-9_.-]+$/.test(value)) out.push(`${key}=${value}`);
|
|
380
|
+
else out.push(`${key}='${value.replace(/'/g, "''")}'`);
|
|
381
|
+
}
|
|
382
|
+
return out.join(", ");
|
|
489
383
|
};
|
|
490
384
|
/** Split a Postgres text-mode array's inner content on top-level commas. */
|
|
491
385
|
const splitArrayElems = (s) => {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return out;
|
|
386
|
+
const out = [];
|
|
387
|
+
let i = 0;
|
|
388
|
+
let cur = "";
|
|
389
|
+
let inQuote = false;
|
|
390
|
+
while (i < s.length) {
|
|
391
|
+
const ch = s[i];
|
|
392
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
393
|
+
cur += s[i] + s[i + 1];
|
|
394
|
+
i += 2;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (ch === "\"") {
|
|
398
|
+
inQuote = !inQuote;
|
|
399
|
+
cur += ch;
|
|
400
|
+
i++;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (ch === "," && !inQuote) {
|
|
404
|
+
out.push(cur);
|
|
405
|
+
cur = "";
|
|
406
|
+
i++;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
cur += ch;
|
|
410
|
+
i++;
|
|
411
|
+
}
|
|
412
|
+
if (cur.length > 0 || s.endsWith(",")) out.push(cur);
|
|
413
|
+
return out;
|
|
521
414
|
};
|
|
522
415
|
/**
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
416
|
+
* Print `buf` with line numbers in the upstream format:
|
|
417
|
+
*
|
|
418
|
+
* - For functions (`isFunc=true`): scan for the first line whose first
|
|
419
|
+
* three / six / seven bytes are `AS `, `BEGIN `, or `RETURN ` and
|
|
420
|
+
* treat that as the start of the body. Header lines (before the
|
|
421
|
+
* marker) render as ` <line>\n`; body lines render as
|
|
422
|
+
* `<lineno><6 spaces> <line>\n` (`%-7d %s\n`), with `lineno` starting
|
|
423
|
+
* at 1 on the marker line.
|
|
424
|
+
* - For views (`isFunc=false`): everything is body; `lineno` starts at
|
|
425
|
+
* 1 and increments per output line.
|
|
426
|
+
*/
|
|
534
427
|
const writeWithLineNumbers = (buf, isFunc, out) => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
out(` ${line}\n`);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
// %-7d → left-justified, padded to 7. Then literal space, then line.
|
|
555
|
-
const numStr = String(lineno);
|
|
556
|
-
const pad = numStr.length >= 7 ? "" : " ".repeat(7 - numStr.length);
|
|
557
|
-
out(`${numStr}${pad} ${line}\n`);
|
|
558
|
-
}
|
|
559
|
-
if (eol === -1)
|
|
560
|
-
break;
|
|
561
|
-
i = eol + 1;
|
|
562
|
-
}
|
|
428
|
+
let inHeader = isFunc;
|
|
429
|
+
let lineno = 0;
|
|
430
|
+
let i = 0;
|
|
431
|
+
while (i < buf.length) {
|
|
432
|
+
const eol = buf.indexOf("\n", i);
|
|
433
|
+
const line = eol === -1 ? buf.slice(i) : buf.slice(i, eol);
|
|
434
|
+
if (inHeader && (line.startsWith("AS ") || line.startsWith("BEGIN ") || line.startsWith("RETURN "))) inHeader = false;
|
|
435
|
+
if (!inHeader) lineno++;
|
|
436
|
+
if (inHeader) out(` ${line}\n`);
|
|
437
|
+
else {
|
|
438
|
+
const numStr = String(lineno);
|
|
439
|
+
out(`${numStr}${numStr.length >= 7 ? "" : " ".repeat(7 - numStr.length)} ${line}\n`);
|
|
440
|
+
}
|
|
441
|
+
if (eol === -1) break;
|
|
442
|
+
i = eol + 1;
|
|
443
|
+
}
|
|
563
444
|
};
|
|
564
445
|
/** Stream the definition (with or without line numbers) to stdout. */
|
|
565
446
|
const emitDefinition = (def, plus, isFunc) => {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
writeOut(def);
|
|
571
|
-
}
|
|
447
|
+
if (plus) writeWithLineNumbers(def, isFunc, writeOut);
|
|
448
|
+
else writeOut(def);
|
|
572
449
|
};
|
|
573
|
-
// ---------------------------------------------------------------------------
|
|
574
|
-
// Shared core: \sf / \ef (function) and \sv / \ev (view).
|
|
575
|
-
// ---------------------------------------------------------------------------
|
|
576
450
|
/**
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
451
|
+
* Resolve a function/view definition and dump it to stdout. Used by
|
|
452
|
+
* both the show forms (`\sf` / `\sv`) and the edit forms (`\ef` / `\ev`)
|
|
453
|
+
* when the user supplies a name.
|
|
454
|
+
*/
|
|
581
455
|
const runShowFunction = async (ctx, cmdName, base) => {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const defLookup = await getFunctionCreateCmd(c, oidLookup.oid);
|
|
594
|
-
if (!defLookup.ok)
|
|
595
|
-
return queryErrResult(ctx, defLookup.err);
|
|
596
|
-
emitDefinition(defLookup.def, plus, /*isFunc=*/ true);
|
|
597
|
-
return { status: "ok" };
|
|
456
|
+
const c = conn(ctx);
|
|
457
|
+
if (!c) return noConn(ctx);
|
|
458
|
+
const { plus } = decodeShowSuffix(cmdName, base);
|
|
459
|
+
const desc = readObjDesc(ctx);
|
|
460
|
+
if (desc === null) return errResult(ctx, "function name is required");
|
|
461
|
+
const oidLookup = await lookupFunctionOid(c, desc);
|
|
462
|
+
if (!oidLookup.ok) return queryErrResult(ctx, oidLookup.err);
|
|
463
|
+
const defLookup = await getFunctionCreateCmd(c, oidLookup.oid);
|
|
464
|
+
if (!defLookup.ok) return queryErrResult(ctx, defLookup.err);
|
|
465
|
+
emitDefinition(defLookup.def, plus, true);
|
|
466
|
+
return { status: "ok" };
|
|
598
467
|
};
|
|
599
468
|
const runShowView = async (ctx, cmdName, base) => {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const defLookup = await getViewCreateCmd(c, oidLookup.oid);
|
|
612
|
-
if (!defLookup.ok)
|
|
613
|
-
return queryErrResult(ctx, defLookup.err);
|
|
614
|
-
emitDefinition(defLookup.def, plus, /*isFunc=*/ false);
|
|
615
|
-
return { status: "ok" };
|
|
469
|
+
const c = conn(ctx);
|
|
470
|
+
if (!c) return noConn(ctx);
|
|
471
|
+
const { plus } = decodeShowSuffix(cmdName, base);
|
|
472
|
+
const desc = readObjDesc(ctx);
|
|
473
|
+
if (desc === null) return errResult(ctx, "view name is required");
|
|
474
|
+
const oidLookup = await lookupRelationOid(c, desc);
|
|
475
|
+
if (!oidLookup.ok) return queryErrResult(ctx, oidLookup.err);
|
|
476
|
+
const defLookup = await getViewCreateCmd(c, oidLookup.oid);
|
|
477
|
+
if (!defLookup.ok) return queryErrResult(ctx, defLookup.err);
|
|
478
|
+
emitDefinition(defLookup.def, plus, false);
|
|
479
|
+
return { status: "ok" };
|
|
616
480
|
};
|
|
617
|
-
// ---------------------------------------------------------------------------
|
|
618
|
-
// BackslashCmdSpec exports
|
|
619
|
-
// ---------------------------------------------------------------------------
|
|
620
481
|
/** `\sf [+] FUNCNAME` — show function source. */
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
482
|
+
const cmdShowFunction = {
|
|
483
|
+
name: "sf",
|
|
484
|
+
argMode: "whole-line",
|
|
485
|
+
helpKey: "sf",
|
|
486
|
+
run: (ctx) => runShowFunction(ctx, ctx.cmdName, "sf")
|
|
626
487
|
};
|
|
627
488
|
/** `\sf+ FUNCNAME` — show function source with line numbers. */
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
489
|
+
const cmdShowFunctionPlus = {
|
|
490
|
+
name: "sf+",
|
|
491
|
+
argMode: "whole-line",
|
|
492
|
+
helpKey: "sf",
|
|
493
|
+
run: (ctx) => runShowFunction(ctx, ctx.cmdName, "sf")
|
|
633
494
|
};
|
|
634
495
|
/** `\sv [+] VIEWNAME` — show view source. */
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
496
|
+
const cmdShowView = {
|
|
497
|
+
name: "sv",
|
|
498
|
+
argMode: "whole-line",
|
|
499
|
+
helpKey: "sv",
|
|
500
|
+
run: (ctx) => runShowView(ctx, ctx.cmdName, "sv")
|
|
640
501
|
};
|
|
641
502
|
/** `\sv+ VIEWNAME` — show view source with line numbers. */
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
503
|
+
const cmdShowViewPlus = {
|
|
504
|
+
name: "sv+",
|
|
505
|
+
argMode: "whole-line",
|
|
506
|
+
helpKey: "sv",
|
|
507
|
+
run: (ctx) => runShowView(ctx, ctx.cmdName, "sv")
|
|
647
508
|
};
|
|
648
509
|
/**
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
return queryErrResult(ctx, oidLookup.err);
|
|
674
|
-
const defLookup = await getFunctionCreateCmd(c, oidLookup.oid);
|
|
675
|
-
if (!defLookup.ok)
|
|
676
|
-
return queryErrResult(ctx, defLookup.err);
|
|
677
|
-
emitDefinition(defLookup.def, plus, /*isFunc=*/ true);
|
|
678
|
-
return { status: "ok" };
|
|
679
|
-
},
|
|
510
|
+
* `\ef [+] [FUNCNAME [LINE]]` — upstream opens `$EDITOR` on the function's
|
|
511
|
+
* source. We don't implement editor invocation; when a name is supplied we
|
|
512
|
+
* route through the same fetch+print path as `\sf` (with a stripped line
|
|
513
|
+
* number — the trailing LINE argument is ignored). Without a name we error
|
|
514
|
+
* with a hint pointing at `\sf`.
|
|
515
|
+
*/
|
|
516
|
+
const cmdEditFunction = {
|
|
517
|
+
name: "ef",
|
|
518
|
+
argMode: "whole-line",
|
|
519
|
+
helpKey: "ef",
|
|
520
|
+
async run(ctx) {
|
|
521
|
+
const c = conn(ctx);
|
|
522
|
+
if (!c) return noConn(ctx);
|
|
523
|
+
const { plus } = decodeShowSuffix(ctx.cmdName, "ef");
|
|
524
|
+
const desc = readObjDesc(ctx);
|
|
525
|
+
if (desc === null) return errResult(ctx, "editing not supported in embedded psql; supply a name to display the source");
|
|
526
|
+
const objDesc = stripTrailingLine(desc);
|
|
527
|
+
const oidLookup = await lookupFunctionOid(c, objDesc);
|
|
528
|
+
if (!oidLookup.ok) return queryErrResult(ctx, oidLookup.err);
|
|
529
|
+
const defLookup = await getFunctionCreateCmd(c, oidLookup.oid);
|
|
530
|
+
if (!defLookup.ok) return queryErrResult(ctx, defLookup.err);
|
|
531
|
+
emitDefinition(defLookup.def, plus, true);
|
|
532
|
+
return { status: "ok" };
|
|
533
|
+
}
|
|
680
534
|
};
|
|
681
535
|
/**
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (!defLookup.ok)
|
|
703
|
-
return queryErrResult(ctx, defLookup.err);
|
|
704
|
-
emitDefinition(defLookup.def, plus, /*isFunc=*/ false);
|
|
705
|
-
return { status: "ok" };
|
|
706
|
-
},
|
|
536
|
+
* `\ev [+] [VIEWNAME [LINE]]` — same contract as `\ef` but for views.
|
|
537
|
+
*/
|
|
538
|
+
const cmdEditView = {
|
|
539
|
+
name: "ev",
|
|
540
|
+
argMode: "whole-line",
|
|
541
|
+
helpKey: "ev",
|
|
542
|
+
async run(ctx) {
|
|
543
|
+
const c = conn(ctx);
|
|
544
|
+
if (!c) return noConn(ctx);
|
|
545
|
+
const { plus } = decodeShowSuffix(ctx.cmdName, "ev");
|
|
546
|
+
const desc = readObjDesc(ctx);
|
|
547
|
+
if (desc === null) return errResult(ctx, "editing not supported in embedded psql; supply a name to display the source");
|
|
548
|
+
const objDesc = stripTrailingLine(desc);
|
|
549
|
+
const oidLookup = await lookupRelationOid(c, objDesc);
|
|
550
|
+
if (!oidLookup.ok) return queryErrResult(ctx, oidLookup.err);
|
|
551
|
+
const defLookup = await getViewCreateCmd(c, oidLookup.oid);
|
|
552
|
+
if (!defLookup.ok) return queryErrResult(ctx, defLookup.err);
|
|
553
|
+
emitDefinition(defLookup.def, plus, false);
|
|
554
|
+
return { status: "ok" };
|
|
555
|
+
}
|
|
707
556
|
};
|
|
708
557
|
/**
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
558
|
+
* Aliases so that `\ef+` and `\ev+` resolve to the corresponding command;
|
|
559
|
+
* we register the plus variants explicitly because the registry is keyed
|
|
560
|
+
* by full name.
|
|
561
|
+
*/
|
|
562
|
+
const cmdEditFunctionPlus = {
|
|
563
|
+
...cmdEditFunction,
|
|
564
|
+
name: "ef+"
|
|
716
565
|
};
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
566
|
+
const cmdEditViewPlus = {
|
|
567
|
+
...cmdEditView,
|
|
568
|
+
name: "ev+"
|
|
720
569
|
};
|
|
721
570
|
/**
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
571
|
+
* Strip a trailing LINE number from an object descriptor, matching
|
|
572
|
+
* upstream's `strip_lineno_from_objdesc`. We rebuild the simpler subset
|
|
573
|
+
* here because the slash-arg scanner already handed us a single trimmed
|
|
574
|
+
* whole-line string: if it ends with `<digits>` separated from the
|
|
575
|
+
* preceding name by whitespace or `)`, strip the digits.
|
|
576
|
+
*
|
|
577
|
+
* Returns the descriptor with any trailing line number removed. Invalid
|
|
578
|
+
* line numbers (zero) are not detected here — we treat them the same as
|
|
579
|
+
* "no line number" because the LINE arg is meaningless to our
|
|
580
|
+
* show-only impl.
|
|
581
|
+
*/
|
|
733
582
|
const stripTrailingLine = (desc) => {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
// very first char.
|
|
743
|
-
if (i <= 0)
|
|
744
|
-
return desc;
|
|
745
|
-
const sep = desc[i];
|
|
746
|
-
if (!(/\s/.test(sep) || sep === ")"))
|
|
747
|
-
return desc;
|
|
748
|
-
return desc.slice(0, i + 1).trimEnd();
|
|
583
|
+
let i = desc.length - 1;
|
|
584
|
+
while (i > 0 && /\s/.test(desc[i])) i--;
|
|
585
|
+
if (i <= 0 || !/[0-9]/.test(desc[i])) return desc;
|
|
586
|
+
while (i > 0 && /[0-9]/.test(desc[i])) i--;
|
|
587
|
+
if (i <= 0) return desc;
|
|
588
|
+
const sep = desc[i];
|
|
589
|
+
if (!(/\s/.test(sep) || sep === ")")) return desc;
|
|
590
|
+
return desc.slice(0, i + 1).trimEnd();
|
|
749
591
|
};
|
|
750
|
-
// ---------------------------------------------------------------------------
|
|
751
|
-
// Registration
|
|
752
|
-
// ---------------------------------------------------------------------------
|
|
753
592
|
/**
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
593
|
+
* Register `\sf`, `\sf+`, `\sv`, `\sv+`, `\ef`, `\ef+`, `\ev`, `\ev+` on
|
|
594
|
+
* the supplied registry. Wired into `defaultRegistry()` from `dispatch.ts`.
|
|
595
|
+
*/
|
|
596
|
+
const registerShowCommands = (registry) => {
|
|
597
|
+
registry.register(cmdShowFunction);
|
|
598
|
+
registry.register(cmdShowFunctionPlus);
|
|
599
|
+
registry.register(cmdShowView);
|
|
600
|
+
registry.register(cmdShowViewPlus);
|
|
601
|
+
registry.register(cmdEditFunction);
|
|
602
|
+
registry.register(cmdEditFunctionPlus);
|
|
603
|
+
registry.register(cmdEditView);
|
|
604
|
+
registry.register(cmdEditViewPlus);
|
|
766
605
|
};
|
|
606
|
+
//#endregion
|
|
607
|
+
export { cmdEditFunction, cmdEditFunctionPlus, cmdEditView, cmdEditViewPlus, cmdShowFunction, cmdShowFunctionPlus, cmdShowView, cmdShowViewPlus, registerShowCommands };
|