neonctl 2.22.2 → 2.23.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 +84 -0
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/connection_string.js +9 -1
- package/commands/functions.js +277 -0
- package/commands/index.js +4 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +6 -1
- package/functions_api.js +44 -0
- package/package.json +15 -5
- package/psql/cli.js +51 -0
- package/psql/command/cmd_cond.js +437 -0
- package/psql/command/cmd_connect.js +815 -0
- package/psql/command/cmd_copy.js +1025 -0
- package/psql/command/cmd_describe.js +1810 -0
- package/psql/command/cmd_format.js +909 -0
- package/psql/command/cmd_io.js +2187 -0
- package/psql/command/cmd_lo.js +385 -0
- package/psql/command/cmd_meta.js +970 -0
- package/psql/command/cmd_misc.js +187 -0
- package/psql/command/cmd_pipeline.js +1141 -0
- package/psql/command/cmd_restrict.js +171 -0
- package/psql/command/cmd_show.js +751 -0
- package/psql/command/dispatch.js +343 -0
- package/psql/command/inputQueue.js +42 -0
- package/psql/command/shared.js +71 -0
- package/psql/complete/filenames.js +139 -0
- package/psql/complete/index.js +104 -0
- package/psql/complete/matcher.js +314 -0
- package/psql/complete/psqlVars.js +247 -0
- package/psql/complete/queries.js +491 -0
- package/psql/complete/rules.js +2387 -0
- package/psql/core/common.js +1250 -0
- package/psql/core/help.js +576 -0
- package/psql/core/mainloop.js +1353 -0
- package/psql/core/prompt.js +437 -0
- package/psql/core/settings.js +684 -0
- package/psql/core/sqlHelp.js +1066 -0
- package/psql/core/startup.js +840 -0
- package/psql/core/syncVars.js +116 -0
- package/psql/core/variables.js +287 -0
- package/psql/describe/formatters.js +1277 -0
- package/psql/describe/processNamePattern.js +270 -0
- package/psql/describe/queries.js +2373 -0
- package/psql/describe/versionGate.js +43 -0
- package/psql/index.js +2005 -0
- package/psql/io/history.js +299 -0
- package/psql/io/input.js +120 -0
- package/psql/io/lineEditor/buffer.js +323 -0
- package/psql/io/lineEditor/complete.js +227 -0
- package/psql/io/lineEditor/filename.js +159 -0
- package/psql/io/lineEditor/index.js +891 -0
- package/psql/io/lineEditor/keymap.js +738 -0
- package/psql/io/lineEditor/vt100.js +363 -0
- package/psql/io/pgpass.js +202 -0
- package/psql/io/pgservice.js +194 -0
- package/psql/io/psqlrc.js +422 -0
- package/psql/print/aligned.js +1756 -0
- package/psql/print/asciidoc.js +248 -0
- package/psql/print/crosstab.js +460 -0
- package/psql/print/csv.js +92 -0
- package/psql/print/html.js +258 -0
- package/psql/print/json.js +96 -0
- package/psql/print/latex.js +396 -0
- package/psql/print/pager.js +265 -0
- package/psql/print/troff.js +258 -0
- package/psql/print/unaligned.js +118 -0
- package/psql/print/units.js +135 -0
- package/psql/scanner/slash.js +513 -0
- package/psql/scanner/sql.js +910 -0
- package/psql/scanner/stringutils.js +390 -0
- package/psql/types/backslash.js +1 -0
- package/psql/types/connection.js +1 -0
- package/psql/types/index.js +7 -0
- package/psql/types/printer.js +1 -0
- package/psql/types/repl.js +1 -0
- package/psql/types/scanner.js +24 -0
- package/psql/types/settings.js +1 -0
- package/psql/types/variables.js +1 -0
- package/psql/wire/connection.js +2844 -0
- package/psql/wire/copy.js +108 -0
- package/psql/wire/notify.js +59 -0
- package/psql/wire/pipeline.js +519 -0
- package/psql/wire/protocol.js +466 -0
- package/psql/wire/sasl.js +296 -0
- package/psql/wire/tls.js +596 -0
- package/test_utils/fixtures.js +1 -0
- package/utils/esbuild.js +147 -0
- package/utils/psql.js +107 -11
- package/utils/zip.js +4 -0
- package/writer.js +1 -1
- package/commands/auth.test.js +0 -211
- package/commands/branches.test.js +0 -460
- package/commands/checkout.test.js +0 -170
- package/commands/connection_string.test.js +0 -196
- package/commands/data_api.test.js +0 -169
- package/commands/databases.test.js +0 -39
- package/commands/help.test.js +0 -9
- package/commands/init.test.js +0 -56
- package/commands/ip_allow.test.js +0 -59
- package/commands/link.test.js +0 -381
- package/commands/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/psql.test.js +0 -49
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/context.test.js +0 -119
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backslash command dispatch.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of the top half of PostgreSQL's `src/bin/psql/command.c`:
|
|
5
|
+
* specifically the `exec_command()` entry point that, given a parsed slash
|
|
6
|
+
* command name, looks up the right handler and runs it with a small per-call
|
|
7
|
+
* context (`BackslashContext`).
|
|
8
|
+
*
|
|
9
|
+
* The upstream is a giant switch statement keyed off the first one-or-two
|
|
10
|
+
* letters of the command. We replace that with a registry of typed
|
|
11
|
+
* `BackslashCmdSpec` records keyed by primary name with a separate alias map.
|
|
12
|
+
* Commands are added by `register()` at construction time; the default
|
|
13
|
+
* registry returned by {@link defaultRegistry} is pre-populated with every
|
|
14
|
+
* command implemented in this WP (meta + format). Later WPs (I/O, connection,
|
|
15
|
+
* describe, large object, pipeline, misc) add their own commands by calling
|
|
16
|
+
* `registry.register(...)` on top.
|
|
17
|
+
*
|
|
18
|
+
* The `BackslashContext` carries:
|
|
19
|
+
*
|
|
20
|
+
* - the parsed command name (without the leading backslash),
|
|
21
|
+
* - the raw post-name remainder of the input line (`rawArgs`),
|
|
22
|
+
* - the current SQL query buffer (`queryBuf`), and
|
|
23
|
+
* - a small `nextArg(mode)` / `restOfLine()` pair backed by
|
|
24
|
+
* `scanSlashArgs()` from the WP-07 scanner. Each call to `nextArg` returns
|
|
25
|
+
* the next lexed argument under the requested {@link SlashArgMode}, or
|
|
26
|
+
* `null` once the buffer is exhausted. Mixing modes across calls is
|
|
27
|
+
* supported: each call rescans the tail starting at the current cursor.
|
|
28
|
+
*
|
|
29
|
+
* Variable substitution: the scanner is given a `varLookup` callback that
|
|
30
|
+
* delegates to `settings.vars`. Modes that disable substitution (`no-vars`)
|
|
31
|
+
* naturally fall through to the scanner's existing behaviour.
|
|
32
|
+
*/
|
|
33
|
+
import { scanSlashArgs } from '../scanner/slash.js';
|
|
34
|
+
import { cmdCd, cmdCopyright, cmdEcho, cmdEdit, cmdErrverbose, cmdGetenv, cmdHelpSQL, cmdPrompt, cmdQecho, cmdQuit, cmdReset, cmdS, cmdSet, cmdSetenv, cmdShell, cmdSlashHelp, cmdTiming, cmdUnset, cmdWarn, } from './cmd_meta.js';
|
|
35
|
+
import { cmdA, cmdC, cmdEncoding, cmdF, cmdH, cmdPset, cmdT, cmdTitleAttr, cmdX, } from './cmd_format.js';
|
|
36
|
+
import { registerIoCommands } from './cmd_io.js';
|
|
37
|
+
import { registerConnectCommands } from './cmd_connect.js';
|
|
38
|
+
import { registerCopyCommands } from './cmd_copy.js';
|
|
39
|
+
import { registerDescribeCommands } from './cmd_describe.js';
|
|
40
|
+
import { registerPipelineCommands } from './cmd_pipeline.js';
|
|
41
|
+
import { registerMiscCommands } from './cmd_misc.js';
|
|
42
|
+
import { registerLargeObjectCommands } from './cmd_lo.js';
|
|
43
|
+
import { registerShowCommands } from './cmd_show.js';
|
|
44
|
+
import { isCommandRestricted, registerRestrictCommands, wrapRestrictedCommands, } from './cmd_restrict.js';
|
|
45
|
+
import { writeErr } from './shared.js';
|
|
46
|
+
/**
|
|
47
|
+
* Concrete `BackslashRegistry`: a primary-name → spec map plus a parallel
|
|
48
|
+
* alias → primary-name map so lookups stay O(1).
|
|
49
|
+
*
|
|
50
|
+
* Re-registering the same primary name overwrites the existing spec; this
|
|
51
|
+
* matches the upstream behaviour that doesn't multi-register and gives
|
|
52
|
+
* downstream WPs a clean way to override a default if they need to.
|
|
53
|
+
*/
|
|
54
|
+
class Registry {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.specs = new Map();
|
|
57
|
+
this.aliases = new Map();
|
|
58
|
+
}
|
|
59
|
+
register(spec) {
|
|
60
|
+
this.specs.set(spec.name, spec);
|
|
61
|
+
if (spec.aliases) {
|
|
62
|
+
for (const alias of spec.aliases) {
|
|
63
|
+
this.aliases.set(alias, spec.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lookup(name) {
|
|
68
|
+
const direct = this.specs.get(name);
|
|
69
|
+
if (direct)
|
|
70
|
+
return direct;
|
|
71
|
+
const aliased = this.aliases.get(name);
|
|
72
|
+
if (aliased)
|
|
73
|
+
return this.specs.get(aliased);
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
all() {
|
|
77
|
+
return this.specs.values();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Construct a fresh, empty registry. */
|
|
81
|
+
export const createBackslashRegistry = () => new Registry();
|
|
82
|
+
/**
|
|
83
|
+
* Build a {@link BackslashContext} from inputs the REPL has on hand at
|
|
84
|
+
* dispatch time.
|
|
85
|
+
*
|
|
86
|
+
* The context's `nextArg(mode)` is built on top of `scanSlashArgs`. We
|
|
87
|
+
* maintain a small internal byte cursor that tracks how much of `rawArgs`
|
|
88
|
+
* has been consumed so far; each call rescans the remaining tail in the
|
|
89
|
+
* requested mode and advances the cursor past the first arg's source
|
|
90
|
+
* extent. `restOfLine()` returns the unconsumed tail verbatim (with leading
|
|
91
|
+
* whitespace trimmed, matching `whole-line` semantics) and advances the
|
|
92
|
+
* cursor to the end.
|
|
93
|
+
*
|
|
94
|
+
* The tracking is conservative: because `scanSlashArgs` does not directly
|
|
95
|
+
* report per-arg source spans, we estimate the consumed span by re-lexing
|
|
96
|
+
* with a 1-arg cap in `whole-line` mode to find the boundary. This is an
|
|
97
|
+
* over-approximation only when adjacent quoted runs collapse to fewer
|
|
98
|
+
* characters in the parsed output — in practice every command in this WP
|
|
99
|
+
* either reads args in order or reads the whole tail with `restOfLine()`,
|
|
100
|
+
* so the cursor is never observed to lag in the calls we ship.
|
|
101
|
+
*/
|
|
102
|
+
export const makeContext = (opts) => {
|
|
103
|
+
let cursor = 0;
|
|
104
|
+
const rawArgs = opts.rawArgs;
|
|
105
|
+
const varLookup = (name) => opts.settings.vars.get(name);
|
|
106
|
+
const nextArg = (mode = 'normal') => {
|
|
107
|
+
// Find the next non-whitespace byte from the cursor; we use it both to
|
|
108
|
+
// know whether anything remains and as the basis for span tracking.
|
|
109
|
+
let i = cursor;
|
|
110
|
+
while (i < rawArgs.length && /[\s]/.test(rawArgs[i]))
|
|
111
|
+
i++;
|
|
112
|
+
if (i >= rawArgs.length)
|
|
113
|
+
return null;
|
|
114
|
+
if (mode === 'whole-line') {
|
|
115
|
+
const tail = rawArgs.slice(i);
|
|
116
|
+
cursor = rawArgs.length;
|
|
117
|
+
return tail;
|
|
118
|
+
}
|
|
119
|
+
// Scan just the tail and pick the first arg. The scanner consumes one
|
|
120
|
+
// arg's worth of input; we need to advance `cursor` past it so the next
|
|
121
|
+
// call sees the remaining tail. We do that by rescanning the tail again
|
|
122
|
+
// with a one-token cap and comparing lengths.
|
|
123
|
+
const tail = rawArgs.slice(i);
|
|
124
|
+
const args = scanSlashArgs(tail, mode, varLookup);
|
|
125
|
+
if (args.length === 0) {
|
|
126
|
+
cursor = rawArgs.length;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const first = args[0];
|
|
130
|
+
// Compute the consumed span by scanning the original tail in normal
|
|
131
|
+
// mode and finding where the second arg would start. We don't have a
|
|
132
|
+
// direct API for that, so we walk character-by-character using the
|
|
133
|
+
// same termination rules as the scanner.
|
|
134
|
+
const span = consumedSpan(tail, mode, varLookup);
|
|
135
|
+
cursor = i + span;
|
|
136
|
+
return first;
|
|
137
|
+
};
|
|
138
|
+
const restOfLine = () => {
|
|
139
|
+
let i = cursor;
|
|
140
|
+
while (i < rawArgs.length && /[\s]/.test(rawArgs[i]))
|
|
141
|
+
i++;
|
|
142
|
+
const tail = rawArgs.slice(i);
|
|
143
|
+
cursor = rawArgs.length;
|
|
144
|
+
return tail;
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
settings: opts.settings,
|
|
148
|
+
cmdName: opts.cmdName,
|
|
149
|
+
queryBuf: opts.queryBuf,
|
|
150
|
+
rawArgs,
|
|
151
|
+
nextArg,
|
|
152
|
+
restOfLine,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Compute how many bytes of `tail` were consumed lexing the first arg. We
|
|
157
|
+
* walk the same quoting/escape rules as the scanner so the cursor advances
|
|
158
|
+
* past the *source* extent, not the post-expansion length.
|
|
159
|
+
*
|
|
160
|
+
* Stops at whitespace or backslash. Quoted runs (`'…'`, `"…"`, `` `…` ``)
|
|
161
|
+
* are consumed to their closing delimiter. `:var` substitutions advance
|
|
162
|
+
* past the original `:name` form regardless of expansion size.
|
|
163
|
+
*/
|
|
164
|
+
const consumedSpan = (tail, mode, varLookup) => {
|
|
165
|
+
if (mode === 'whole-line')
|
|
166
|
+
return tail.length;
|
|
167
|
+
let i = 0;
|
|
168
|
+
// Skip leading whitespace inside the tail (already trimmed by caller, but
|
|
169
|
+
// safe to repeat).
|
|
170
|
+
while (i < tail.length && /[\s]/.test(tail[i]))
|
|
171
|
+
i++;
|
|
172
|
+
// filepipe special: a leading `|` slurps to EOL.
|
|
173
|
+
if (mode === 'filepipe' && tail[i] === '|')
|
|
174
|
+
return tail.length;
|
|
175
|
+
while (i < tail.length) {
|
|
176
|
+
const c = tail[i];
|
|
177
|
+
if (/[\s]/.test(c) || c === '\\')
|
|
178
|
+
break;
|
|
179
|
+
if (c === "'") {
|
|
180
|
+
i++;
|
|
181
|
+
while (i < tail.length) {
|
|
182
|
+
if (tail[i] === '\\' && i + 1 < tail.length) {
|
|
183
|
+
i += 2;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (tail[i] === "'") {
|
|
187
|
+
if (tail[i + 1] === "'") {
|
|
188
|
+
i += 2;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
i++;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (c === '"') {
|
|
199
|
+
i++;
|
|
200
|
+
while (i < tail.length && tail[i] !== '"')
|
|
201
|
+
i++;
|
|
202
|
+
if (i < tail.length)
|
|
203
|
+
i++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (c === '`') {
|
|
207
|
+
i++;
|
|
208
|
+
while (i < tail.length && tail[i] !== '`')
|
|
209
|
+
i++;
|
|
210
|
+
if (i < tail.length)
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (c === ':' && mode !== 'no-vars') {
|
|
215
|
+
// :"name" / :'name' / :name — advance past the source form. We don't
|
|
216
|
+
// actually call varLookup here; we just measure the lexical span.
|
|
217
|
+
void varLookup;
|
|
218
|
+
const next = tail[i + 1];
|
|
219
|
+
if (next === '"' || next === "'") {
|
|
220
|
+
let j = i + 2;
|
|
221
|
+
while (j < tail.length && /[A-Za-z0-9_\x80-\xff]/.test(tail[j]))
|
|
222
|
+
j++;
|
|
223
|
+
if (j > i + 2 && tail[j] === next) {
|
|
224
|
+
i = j + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (next && /[A-Za-z0-9_\x80-\xff]/.test(next)) {
|
|
229
|
+
let j = i + 1;
|
|
230
|
+
while (j < tail.length && /[A-Za-z0-9_\x80-\xff]/.test(tail[j]))
|
|
231
|
+
j++;
|
|
232
|
+
i = j;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
i++;
|
|
237
|
+
}
|
|
238
|
+
return i;
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Top-level dispatch entry. Looks the command up by name (and falls back to
|
|
242
|
+
* registered aliases), runs it, and returns the result.
|
|
243
|
+
*
|
|
244
|
+
* Unknown commands return `{ status: 'error' }` so the mainloop can emit
|
|
245
|
+
* the upstream-style `"invalid command \…"` diagnostic. We deliberately
|
|
246
|
+
* don't print here; the caller owns stderr.
|
|
247
|
+
*/
|
|
248
|
+
export const dispatchBackslash = async (registry, cmdName, ctx) => {
|
|
249
|
+
const spec = registry.lookup(cmdName);
|
|
250
|
+
if (!spec)
|
|
251
|
+
return { status: 'error' };
|
|
252
|
+
// PG 18: refuse shell/filesystem-touching commands while restricted.
|
|
253
|
+
// We check against the resolved *primary* name so aliases like
|
|
254
|
+
// `\write` → `w` are caught.
|
|
255
|
+
if (isCommandRestricted(ctx.settings, spec.name)) {
|
|
256
|
+
writeErr(`\\${cmdName}: command is not allowed in restricted mode; ` +
|
|
257
|
+
`use \\unrestrict to leave restricted mode\n`);
|
|
258
|
+
return { status: 'error' };
|
|
259
|
+
}
|
|
260
|
+
return spec.run(ctx);
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Return a fresh registry pre-populated with every backslash command this
|
|
264
|
+
* WP implements: meta (`\q`, `\r`/`\reset`, `\!`, `\cd`, `\echo`, `\qecho`,
|
|
265
|
+
* `\warn`, `\prompt`, `\set`, `\unset`, `\getenv`, `\setenv`, `\errverbose`,
|
|
266
|
+
* `\timing`) and format (`\a`, `\C`, `\f`, `\H`, `\t`, `\T`, `\x`,
|
|
267
|
+
* `\pset`, `\encoding`).
|
|
268
|
+
*
|
|
269
|
+
* Other WPs (15/17/20/21/22/23) extend this set by calling `register()` on
|
|
270
|
+
* the returned registry — see the plan for the full mapping.
|
|
271
|
+
*/
|
|
272
|
+
export const defaultRegistry = () => {
|
|
273
|
+
const r = createBackslashRegistry();
|
|
274
|
+
// Meta.
|
|
275
|
+
r.register(cmdQuit);
|
|
276
|
+
r.register(cmdReset);
|
|
277
|
+
r.register(cmdShell);
|
|
278
|
+
r.register(cmdCd);
|
|
279
|
+
r.register(cmdEcho);
|
|
280
|
+
r.register(cmdQecho);
|
|
281
|
+
r.register(cmdWarn);
|
|
282
|
+
r.register(cmdPrompt);
|
|
283
|
+
r.register(cmdSet);
|
|
284
|
+
r.register(cmdUnset);
|
|
285
|
+
r.register(cmdGetenv);
|
|
286
|
+
r.register(cmdSetenv);
|
|
287
|
+
r.register(cmdErrverbose);
|
|
288
|
+
r.register(cmdTiming);
|
|
289
|
+
r.register(cmdCopyright);
|
|
290
|
+
r.register(cmdHelpSQL);
|
|
291
|
+
// `\?` (backslash-command help), `\e`/`\edit` (edit query buffer in an
|
|
292
|
+
// external editor), and `\s` (print/save command history) are full
|
|
293
|
+
// implementations living in `cmd_meta.ts`. They previously sat here as
|
|
294
|
+
// no-op stubs only so the `\if false ... <cmd> ... \endif` inactive-branch
|
|
295
|
+
// enumeration didn't emit spurious "invalid command" diagnostics; now that
|
|
296
|
+
// they do real work the inactive-branch guard still skips them (it only
|
|
297
|
+
// checks that the name is registered).
|
|
298
|
+
r.register(cmdSlashHelp);
|
|
299
|
+
r.register(cmdEdit);
|
|
300
|
+
r.register(cmdS);
|
|
301
|
+
// `\html` is NOT a real psql command (HTML output is the `\H` toggle in
|
|
302
|
+
// `cmd_format.ts`) — but it MUST stay registered as a recognized no-op.
|
|
303
|
+
// Upstream `psql.sql`'s inactive-branch enumeration test (`\if false …
|
|
304
|
+
// \html … \endif`, regress line 1062) requires every backslash name in
|
|
305
|
+
// that dump to be recognized: our inactive-branch guard skips a command
|
|
306
|
+
// only when its name is registered, and silently emits "invalid command"
|
|
307
|
+
// otherwise. An unregistered `\html` therefore breaks the regress diff.
|
|
308
|
+
// Upstream skips ALL commands (known or not) in a false branch; until our
|
|
309
|
+
// mainloop matches that, the recognized-name stub is the load-bearing
|
|
310
|
+
// shim. (In an active branch this makes `\html` a silent no-op rather than
|
|
311
|
+
// upstream's "invalid command", but no test exercises that path.)
|
|
312
|
+
r.register({
|
|
313
|
+
name: 'html',
|
|
314
|
+
run: () => Promise.resolve({ status: 'ok' }),
|
|
315
|
+
});
|
|
316
|
+
// Format.
|
|
317
|
+
r.register(cmdA);
|
|
318
|
+
r.register(cmdC);
|
|
319
|
+
r.register(cmdF);
|
|
320
|
+
r.register(cmdH);
|
|
321
|
+
r.register(cmdT);
|
|
322
|
+
r.register(cmdTitleAttr);
|
|
323
|
+
r.register(cmdX);
|
|
324
|
+
r.register(cmdEncoding);
|
|
325
|
+
r.register(cmdPset);
|
|
326
|
+
// I/O & control (WP-15).
|
|
327
|
+
registerIoCommands(r);
|
|
328
|
+
registerConnectCommands(r);
|
|
329
|
+
registerCopyCommands(r);
|
|
330
|
+
registerDescribeCommands(r);
|
|
331
|
+
registerPipelineCommands(r);
|
|
332
|
+
registerMiscCommands(r);
|
|
333
|
+
registerLargeObjectCommands(r);
|
|
334
|
+
registerShowCommands(r);
|
|
335
|
+
registerRestrictCommands(r);
|
|
336
|
+
// Must run after every other `register*` call so the wrappers see the
|
|
337
|
+
// final specs for the restricted command names (e.g. `\!`, `\cd`, `\copy`,
|
|
338
|
+
// `\setenv`, `\w`). Without this, the REPL mainloop's direct
|
|
339
|
+
// `spec.run(ctx)` invocation bypasses the gate that lives in
|
|
340
|
+
// `dispatchBackslash`.
|
|
341
|
+
wrapRestrictedCommands(r);
|
|
342
|
+
return r;
|
|
343
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending-input queue for `\i` / `\include`.
|
|
3
|
+
*
|
|
4
|
+
* psql's `process_file()` switches the input source for the duration of an
|
|
5
|
+
* included file: while a file is being processed, every subsequent input
|
|
6
|
+
* line comes from the file, not the terminal. The natural place for that
|
|
7
|
+
* switch is the mainloop (`MainLoop` in upstream `mainloop.c`).
|
|
8
|
+
*
|
|
9
|
+
* For WP-15 we keep `src/psql/core/mainloop.ts` untouched. Instead we expose
|
|
10
|
+
* a tiny module-local queue. `\i` enqueues the contents of the included
|
|
11
|
+
* file via {@link enqueue}; a future WP modifies the mainloop's line-source
|
|
12
|
+
* to drain from {@link consumeNext} before reading more user input.
|
|
13
|
+
*
|
|
14
|
+
* Behaviour:
|
|
15
|
+
*
|
|
16
|
+
* - The queue stores file contents as raw strings (typically containing
|
|
17
|
+
* multiple newline-separated SQL statements). Order is FIFO.
|
|
18
|
+
* - {@link consumeNext} returns the head, or `null` if the queue is empty.
|
|
19
|
+
* - {@link reset} clears the queue (used by tests and by any future error
|
|
20
|
+
* recovery path that wants to abandon pending input).
|
|
21
|
+
*
|
|
22
|
+
* The queue is module-scoped because it represents the include stack of a
|
|
23
|
+
* single REPL. Tests should always call {@link reset} in their afterEach so
|
|
24
|
+
* a leftover entry doesn't contaminate the next test.
|
|
25
|
+
*/
|
|
26
|
+
const pending = [];
|
|
27
|
+
/** Append a string of input to the back of the queue. */
|
|
28
|
+
export const enqueue = (content) => {
|
|
29
|
+
pending.push(content);
|
|
30
|
+
};
|
|
31
|
+
/** Return and remove the next pending input, or `null` if none. */
|
|
32
|
+
export const consumeNext = () => {
|
|
33
|
+
if (pending.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
return pending.shift() ?? null;
|
|
36
|
+
};
|
|
37
|
+
/** Number of items currently in the queue. */
|
|
38
|
+
export const size = () => pending.length;
|
|
39
|
+
/** Empty the queue. Tests should call this in cleanup. */
|
|
40
|
+
export const reset = () => {
|
|
41
|
+
pending.length = 0;
|
|
42
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers shared by `cmd_meta.ts` and `cmd_format.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Kept deliberately tiny: just stream writes and the boolean coercion
|
|
5
|
+
* shared by `\timing`, `\t`, `\x`, and `\pset` toggles. The implementations
|
|
6
|
+
* mirror the relevant pieces of upstream `command.c` (`ParseVariableBool`)
|
|
7
|
+
* and `print.c` / `print_aligned*` without depending on either.
|
|
8
|
+
*
|
|
9
|
+
* Why a shared file: the WP spec asks for cmd-isolated test factories, not
|
|
10
|
+
* for the command implementations themselves to duplicate one-line
|
|
11
|
+
* primitives. Going through these helpers also keeps the eslint
|
|
12
|
+
* `no-console` rule satisfied — every write touches `process.stdout` /
|
|
13
|
+
* `process.stderr` directly rather than `console.log` / `console.error`.
|
|
14
|
+
*/
|
|
15
|
+
/** Write to stdout. */
|
|
16
|
+
export const writeOut = (s) => {
|
|
17
|
+
process.stdout.write(s);
|
|
18
|
+
};
|
|
19
|
+
/** Write to stderr. */
|
|
20
|
+
export const writeErr = (s) => {
|
|
21
|
+
process.stderr.write(s);
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Parse a psql boolean the way `ParseVariableBool` does — case-insensitive
|
|
25
|
+
* unique-prefix match against `true|false|yes|no|on|off`, plus `1`/`0`.
|
|
26
|
+
*
|
|
27
|
+
* Returns `null` for unrecognised input.
|
|
28
|
+
*/
|
|
29
|
+
export const parseBool = (raw) => {
|
|
30
|
+
if (raw.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
const lower = raw.toLowerCase();
|
|
33
|
+
const startsWith = (target) => lower.length <= target.length && target.startsWith(lower);
|
|
34
|
+
if (startsWith('true'))
|
|
35
|
+
return true;
|
|
36
|
+
if (startsWith('false'))
|
|
37
|
+
return false;
|
|
38
|
+
if (startsWith('yes'))
|
|
39
|
+
return true;
|
|
40
|
+
if (startsWith('no'))
|
|
41
|
+
return false;
|
|
42
|
+
if (lower.length >= 2) {
|
|
43
|
+
if ('on'.startsWith(lower))
|
|
44
|
+
return true;
|
|
45
|
+
if ('off'.startsWith(lower))
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (raw === '1')
|
|
49
|
+
return true;
|
|
50
|
+
if (raw === '0')
|
|
51
|
+
return false;
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
export const parseTriple = (raw) => {
|
|
55
|
+
const lower = raw.toLowerCase();
|
|
56
|
+
if (lower.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
// Resolve booleans FIRST. Otherwise `t` matched the `toggle` prefix before
|
|
59
|
+
// parseBool, so `\x t` toggled rather than turning expanded ON, and `\pset`
|
|
60
|
+
// bool prefixes were inverted (review: minor divergences).
|
|
61
|
+
const b = parseBool(raw);
|
|
62
|
+
if (b === true)
|
|
63
|
+
return 'on';
|
|
64
|
+
if (b === false)
|
|
65
|
+
return 'off';
|
|
66
|
+
if ('auto'.startsWith(lower))
|
|
67
|
+
return 'auto';
|
|
68
|
+
if ('toggle'.startsWith(lower))
|
|
69
|
+
return 'toggle';
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem-driven completion candidates for `\lo_*`, `\copy ... FROM/TO`,
|
|
3
|
+
* and SQL `COPY ... FROM/TO`.
|
|
4
|
+
*
|
|
5
|
+
* Upstream psql implements this via readline's `rl_filename_completion_function`
|
|
6
|
+
* plus a couple of custom `complete_from_files*` wrappers. We don't have
|
|
7
|
+
* readline; we re-implement the bits we need with `fs.readdirSync`.
|
|
8
|
+
*
|
|
9
|
+
* The completer enumerates entries in the directory referenced by the
|
|
10
|
+
* partial input, filters by basename prefix, and returns *full* candidates
|
|
11
|
+
* (path + basename, matching what the user typed). Directories get a
|
|
12
|
+
* trailing `/` so the editor's `shouldAppendSpace` keeps the user typing
|
|
13
|
+
* through them.
|
|
14
|
+
*
|
|
15
|
+
* For the SQL `COPY ... FROM/TO` context — where the filename must be a
|
|
16
|
+
* string literal — the candidates are wrapped in single quotes. Closing
|
|
17
|
+
* quotes are added only when the candidate is a final filename (unique
|
|
18
|
+
* match), so the line editor's "balanced quotes → append space" rule
|
|
19
|
+
* fires; partial multi-candidate prefixes leave the closing quote off so
|
|
20
|
+
* the user can keep typing.
|
|
21
|
+
*/
|
|
22
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
23
|
+
import { join, basename, dirname } from 'node:path';
|
|
24
|
+
/**
|
|
25
|
+
* Enumerate filesystem entries matching the partial path the user typed.
|
|
26
|
+
*
|
|
27
|
+
* `currentWord` is the raw token (post-tokenizer). It may contain a leading
|
|
28
|
+
* `'` (when the user is already inside a single-quoted SQL literal). The
|
|
29
|
+
* function strips that, looks up the dirname/basename, and returns full
|
|
30
|
+
* candidates that the line editor can splice in with `replaceLength = code
|
|
31
|
+
* points in currentWord`.
|
|
32
|
+
*
|
|
33
|
+
* Returns an empty array on any filesystem error (e.g. directory doesn't
|
|
34
|
+
* exist, no read permission). Tab completion is best-effort — failing to
|
|
35
|
+
* complete is the same as "no candidates".
|
|
36
|
+
*/
|
|
37
|
+
export const completeFilenames = (currentWord, quoteCtx, cwd = process.cwd()) => {
|
|
38
|
+
// Strip an opening single quote (the SQL string-literal case) before
|
|
39
|
+
// resolving the path. The tokenizer keeps it as part of the word.
|
|
40
|
+
let raw = currentWord;
|
|
41
|
+
let hadOpeningSingleQuote = false;
|
|
42
|
+
if (raw.startsWith("'")) {
|
|
43
|
+
hadOpeningSingleQuote = true;
|
|
44
|
+
raw = raw.slice(1);
|
|
45
|
+
}
|
|
46
|
+
// Split into dir + basename prefix. A trailing `/` means "enumerate this
|
|
47
|
+
// dir" and basename prefix is empty.
|
|
48
|
+
const lastSlash = raw.lastIndexOf('/');
|
|
49
|
+
const dirPart = lastSlash === -1 ? '' : raw.slice(0, lastSlash + 1);
|
|
50
|
+
const basePrefix = lastSlash === -1 ? raw : raw.slice(lastSlash + 1);
|
|
51
|
+
// Resolve the directory to scan. Empty `dirPart` → cwd; otherwise it's
|
|
52
|
+
// taken relative to cwd (or absolute if starts with `/`).
|
|
53
|
+
const scanDir = dirPart === ''
|
|
54
|
+
? cwd
|
|
55
|
+
: dirPart.startsWith('/')
|
|
56
|
+
? dirPart
|
|
57
|
+
: join(cwd, dirPart);
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = readdirSync(scanDir);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
// Filter by prefix. Filesystem matching is case-sensitive on Linux/macOS
|
|
66
|
+
// (case-insensitive on macOS by default, but we mirror upstream readline's
|
|
67
|
+
// behaviour which honours the OS's path semantics — case-sensitive on
|
|
68
|
+
// POSIX, which is what the conformance suite runs on).
|
|
69
|
+
const filtered = entries.filter((e) => e.startsWith(basePrefix));
|
|
70
|
+
// Sort alphabetically so the listing is predictable.
|
|
71
|
+
filtered.sort();
|
|
72
|
+
// Build the candidates. Each is `dirPart + entry` (full path matching
|
|
73
|
+
// what the user typed) plus optional trailing `/` for directories.
|
|
74
|
+
const candidates = [];
|
|
75
|
+
for (const entry of filtered) {
|
|
76
|
+
let isDir = false;
|
|
77
|
+
try {
|
|
78
|
+
isDir = statSync(join(scanDir, entry)).isDirectory();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Broken symlink etc. — treat as regular file.
|
|
82
|
+
}
|
|
83
|
+
const full = dirPart + entry + (isDir ? '/' : '');
|
|
84
|
+
candidates.push(full);
|
|
85
|
+
}
|
|
86
|
+
if (quoteCtx === 'none') {
|
|
87
|
+
// Bare paths. Preserve any opening single quote the user already typed
|
|
88
|
+
// (rare for the no-quote contexts, but harmless to mirror).
|
|
89
|
+
if (hadOpeningSingleQuote) {
|
|
90
|
+
return candidates.map((c) => "'" + c);
|
|
91
|
+
}
|
|
92
|
+
return candidates;
|
|
93
|
+
}
|
|
94
|
+
// SQL string-literal context: wrap candidates in single quotes. The
|
|
95
|
+
// line editor's `shouldAppendSpace` checks quote balance — unique
|
|
96
|
+
// candidates close the quote (so `'...'` balances → trailing space
|
|
97
|
+
// fires), multi-candidate common prefixes leave the closing quote off.
|
|
98
|
+
if (candidates.length === 1 && !candidates[0].endsWith('/')) {
|
|
99
|
+
// Unique file (not directory): close the quote so the trailing space
|
|
100
|
+
// fires. Opening quote: re-add if user typed it, else add ourselves.
|
|
101
|
+
return ["'" + candidates[0] + "'"];
|
|
102
|
+
}
|
|
103
|
+
// Multiple candidates OR directory match: opening quote only; closing
|
|
104
|
+
// quote is deferred so the user keeps typing.
|
|
105
|
+
return candidates.map((c) => "'" + c);
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Helper used by `rules.ts` to decide whether the SQL `COPY` we're
|
|
109
|
+
* completing for is a `FROM` (input file) or `TO` (output file). Returns
|
|
110
|
+
* `true` for either — both want filename completion.
|
|
111
|
+
*
|
|
112
|
+
* `prevWords` here is the full prev-words token list. We look for the
|
|
113
|
+
* pattern `COPY <table>+ [FROM|TO]` anywhere as a tail match.
|
|
114
|
+
*/
|
|
115
|
+
export const isCopyFromOrTo = (prevWords) => {
|
|
116
|
+
if (prevWords.length < 3)
|
|
117
|
+
return false;
|
|
118
|
+
// Walk from the end backward: the immediate prev word must be FROM or TO,
|
|
119
|
+
// and somewhere earlier must be COPY (case-insensitive).
|
|
120
|
+
const last = prevWords[prevWords.length - 1].toUpperCase();
|
|
121
|
+
if (last !== 'FROM' && last !== 'TO')
|
|
122
|
+
return false;
|
|
123
|
+
for (let i = prevWords.length - 2; i >= 0; i--) {
|
|
124
|
+
if (prevWords[i].toUpperCase() === 'COPY')
|
|
125
|
+
return true;
|
|
126
|
+
// If we walk past the start of statement (e.g. another keyword like
|
|
127
|
+
// SELECT) we abort — only the SQL `COPY` form should match.
|
|
128
|
+
if (prevWords[i].toUpperCase() === 'SELECT' ||
|
|
129
|
+
prevWords[i].toUpperCase() === 'INSERT' ||
|
|
130
|
+
prevWords[i].toUpperCase() === 'UPDATE' ||
|
|
131
|
+
prevWords[i].toUpperCase() === 'DELETE' ||
|
|
132
|
+
prevWords[i].toUpperCase() === 'WITH') {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
};
|
|
138
|
+
// Re-export for tests.
|
|
139
|
+
export const _internals = { basename, dirname };
|