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