neonctl 2.22.0 → 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 +242 -16
- package/analytics.js +5 -2
- package/commands/branches.js +9 -1
- package/commands/checkout.js +249 -0
- package/commands/connection_string.js +15 -2
- package/commands/data_api.js +286 -0
- package/commands/functions.js +277 -0
- package/commands/index.js +12 -0
- package/commands/link.js +667 -0
- package/commands/neon_auth.js +1013 -0
- package/commands/projects.js +9 -1
- package/commands/psql.js +62 -0
- package/commands/set_context.js +7 -2
- package/context.js +86 -14
- package/functions_api.js +44 -0
- package/index.js +3 -0
- package/package.json +60 -51
- 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/enrichers.js +18 -1
- package/utils/esbuild.js +147 -0
- package/utils/middlewares.js +1 -1
- 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/connection_string.test.js +0 -196
- 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/operations.test.js +0 -7
- package/commands/orgs.test.js +0 -7
- package/commands/projects.test.js +0 -144
- package/commands/roles.test.js +0 -37
- package/commands/set_context.test.js +0 -159
- package/commands/vpc_endpoints.test.js +0 -69
- package/env.test.js +0 -55
- package/utils/formats.test.js +0 -32
- package/writer.test.js +0 -104
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* psql conditional backslash commands: `\if`, `\elif`, `\else`, `\endif`.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of `exec_command_if`/`exec_command_elif`/`exec_command_else`/
|
|
5
|
+
* `exec_command_endif` in `src/bin/psql/command.c`, plus the `ConditionalStack`
|
|
6
|
+
* machinery from `src/fe_utils/conditional.c`.
|
|
7
|
+
*
|
|
8
|
+
* Semantics (mirroring upstream exactly):
|
|
9
|
+
*
|
|
10
|
+
* - `\if <expr>`
|
|
11
|
+
* • If outer branch is active, push a new frame whose state is `TRUE` or
|
|
12
|
+
* `FALSE` depending on the parsed expression value.
|
|
13
|
+
* • Otherwise push `IGNORED`: every nested branch is suppressed regardless
|
|
14
|
+
* of expression. This is how upstream achieves transitive suppression
|
|
15
|
+
* without `conditional_active()` itself being transitive.
|
|
16
|
+
* - `\elif <expr>`
|
|
17
|
+
* • Top is `TRUE` → branch already taken; skip rest until `\endif`
|
|
18
|
+
* (poke to `IGNORED`).
|
|
19
|
+
* • Top is `FALSE` → first true branch wins; evaluate expression.
|
|
20
|
+
* • Top is `IGNORED` → leave untouched, ignore expression.
|
|
21
|
+
* • Top is `ELSE_*` → error: `\elif: cannot occur after \else`.
|
|
22
|
+
* • Top is `NONE` (empty) → error: `\elif: no matching \if`.
|
|
23
|
+
* - `\else`
|
|
24
|
+
* • Top is `TRUE` → poke `ELSE_FALSE`.
|
|
25
|
+
* • Top is `FALSE` → poke `ELSE_TRUE`.
|
|
26
|
+
* • Top is `IGNORED` → poke `ELSE_FALSE` (still suppressed).
|
|
27
|
+
* • Top is `ELSE_*` → error: `\else: cannot occur after \else`.
|
|
28
|
+
* • Top is `NONE` → error: `\else: no matching \if`.
|
|
29
|
+
* - `\endif`
|
|
30
|
+
* • Pop the top frame.
|
|
31
|
+
* • Top was `NONE` → error: `\endif: no matching \if`.
|
|
32
|
+
*
|
|
33
|
+
* Expression evaluation: upstream `read_boolean_expression` reads tokens
|
|
34
|
+
* with `OT_NORMAL` (which expands `:vars` and backticks) and concatenates
|
|
35
|
+
* them with single spaces; the assembled string is passed through
|
|
36
|
+
* `ParseVariableBool`. We mirror that pipeline in {@link collectExpr} +
|
|
37
|
+
* {@link parseBool}: collect every `nextArg('normal')` token, join with
|
|
38
|
+
* spaces, parse. Unrecognised tokens emit the upstream
|
|
39
|
+
* `unrecognized value "<tok>" for "\<cmd> expression": Boolean expected`
|
|
40
|
+
* diagnostic and evaluate to false.
|
|
41
|
+
*
|
|
42
|
+
* Inactive branches: when the surrounding scope is suppressed, upstream
|
|
43
|
+
* `ignore_boolean_expression` drops the argument tokens WITHOUT running
|
|
44
|
+
* `:var` / backtick expansion (regress psql.sql ~line 1028 covers this).
|
|
45
|
+
* We achieve the same by NOT calling `nextArg` at all — the
|
|
46
|
+
* BackslashContext factory only invokes the slash scanner lazily on the
|
|
47
|
+
* first `nextArg` request, so leaving the args queue untouched is
|
|
48
|
+
* equivalent to upstream's "discard without expansion".
|
|
49
|
+
*
|
|
50
|
+
* Diagnostic format: cond commands emit their errors BARE (no
|
|
51
|
+
* `psql: ERROR:` prefix). This mirrors `expected/psql.out` (e.g.
|
|
52
|
+
* `\endif: no matching \if` on a single line). We use `writeErr` directly
|
|
53
|
+
* and return `errorWritten: true` so the mainloop's `psql: ERROR:` fallback
|
|
54
|
+
* is suppressed.
|
|
55
|
+
*
|
|
56
|
+
* Note on the "transitive suppression" requirement: upstream's
|
|
57
|
+
* `conditional_active()` only inspects the top frame, but the resulting state
|
|
58
|
+
* machine *is* transitive because `\if` inside an inactive outer always pushes
|
|
59
|
+
* `IGNORED`, and `IGNORED` never transitions to `TRUE`. So `isActive()`
|
|
60
|
+
* inspecting just the top frame produces the right answer.
|
|
61
|
+
*/
|
|
62
|
+
import { writeErr } from './shared.js';
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Conditional stack
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
const INACTIVE_STATES = ['false', 'else-false', 'ignored'];
|
|
67
|
+
/**
|
|
68
|
+
* Build an empty {@link CondStack}.
|
|
69
|
+
*
|
|
70
|
+
* Frames are stored in an array; `top()` is the last element. `branchTaken`
|
|
71
|
+
* records whether a `TRUE`/`ELSE_TRUE` branch has been seen at this level —
|
|
72
|
+
* mainloop and the elif/else commands use it implicitly via the state-machine
|
|
73
|
+
* transitions described above (we don't expose it through the public API, but
|
|
74
|
+
* it's part of the frame shape declared in `types/repl.ts`).
|
|
75
|
+
*/
|
|
76
|
+
export const createCondStack = () => {
|
|
77
|
+
const frames = [];
|
|
78
|
+
const branchTakenForInitial = (state) => state === 'true' || state === 'else-true';
|
|
79
|
+
return {
|
|
80
|
+
push(state, savedQueryBufLen = 0) {
|
|
81
|
+
frames.push({
|
|
82
|
+
state,
|
|
83
|
+
branchTaken: branchTakenForInitial(state),
|
|
84
|
+
savedQueryBufLen,
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
pop() {
|
|
88
|
+
return frames.pop();
|
|
89
|
+
},
|
|
90
|
+
top() {
|
|
91
|
+
return frames.length === 0 ? undefined : frames[frames.length - 1];
|
|
92
|
+
},
|
|
93
|
+
isActive() {
|
|
94
|
+
// Upstream `conditional_active()`: top is NONE/TRUE/ELSE_TRUE → active.
|
|
95
|
+
// The transitive suppression is encoded by `\if` pushing `IGNORED` when
|
|
96
|
+
// its surrounding context is inactive — see cmdIf below.
|
|
97
|
+
if (frames.length === 0)
|
|
98
|
+
return true;
|
|
99
|
+
return !INACTIVE_STATES.includes(frames[frames.length - 1].state);
|
|
100
|
+
},
|
|
101
|
+
setState(state) {
|
|
102
|
+
if (frames.length === 0)
|
|
103
|
+
return;
|
|
104
|
+
const top = frames[frames.length - 1];
|
|
105
|
+
top.state = state;
|
|
106
|
+
if (state === 'true' || state === 'else-true')
|
|
107
|
+
top.branchTaken = true;
|
|
108
|
+
},
|
|
109
|
+
setSavedQueryBufLen(len) {
|
|
110
|
+
if (frames.length === 0)
|
|
111
|
+
return;
|
|
112
|
+
frames[frames.length - 1].savedQueryBufLen = len;
|
|
113
|
+
},
|
|
114
|
+
depth() {
|
|
115
|
+
return frames.length;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// ParseVariableBool — mirrors variables.c::ParseVariableBool.
|
|
121
|
+
//
|
|
122
|
+
// We re-implement it here (rather than importing from core/variables.ts) so
|
|
123
|
+
// the slash-cmd modules stay decoupled from the var-store implementation;
|
|
124
|
+
// they just need a value-parser. Recognised forms are case-insensitive with
|
|
125
|
+
// unique-prefix matching for word forms:
|
|
126
|
+
//
|
|
127
|
+
// true / false / yes / no (unique-prefix accepted)
|
|
128
|
+
// on / off (need at least 2 chars; bare 'o' ambiguous)
|
|
129
|
+
// 1 / 0 (literal)
|
|
130
|
+
//
|
|
131
|
+
// Anything else is an error (upstream prints a warning and returns false). We
|
|
132
|
+
// follow upstream by treating unrecognised tokens as `false` while pushing
|
|
133
|
+
// the frame.
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
const isPrefixOf = (value, prefix) => value.length > 0 &&
|
|
136
|
+
value.length <= prefix.length &&
|
|
137
|
+
prefix.slice(0, value.length).toLowerCase() === value.toLowerCase();
|
|
138
|
+
/** Returns the parsed boolean, or `null` if the string was not recognised. */
|
|
139
|
+
export const parseBool = (value) => {
|
|
140
|
+
if (value.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
if (isPrefixOf(value, 'true'))
|
|
143
|
+
return true;
|
|
144
|
+
if (isPrefixOf(value, 'false'))
|
|
145
|
+
return false;
|
|
146
|
+
if (isPrefixOf(value, 'yes'))
|
|
147
|
+
return true;
|
|
148
|
+
if (isPrefixOf(value, 'no'))
|
|
149
|
+
return false;
|
|
150
|
+
if (value.length >= 2) {
|
|
151
|
+
const lower = value.toLowerCase();
|
|
152
|
+
if ('on'.startsWith(lower))
|
|
153
|
+
return true;
|
|
154
|
+
if ('off'.startsWith(lower))
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (value === '1')
|
|
158
|
+
return true;
|
|
159
|
+
if (value === '0')
|
|
160
|
+
return false;
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Backslash command implementations
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
/**
|
|
167
|
+
* Read every remaining `'normal'`-mode argument off the BackslashContext and
|
|
168
|
+
* concatenate them with single spaces. Mirrors upstream
|
|
169
|
+
* `read_boolean_expression`, which calls `psql_scan_slash_option(OT_NORMAL)`
|
|
170
|
+
* in a loop and joins tokens with `" "`. Returns the empty string when no
|
|
171
|
+
* args follow the command name — upstream's "missing expression" path
|
|
172
|
+
* surfaces the same `unrecognized value ""...` diagnostic as any other
|
|
173
|
+
* unparseable token, then evaluates to false.
|
|
174
|
+
*
|
|
175
|
+
* After joining, we resolve the `:{?varname}` "defined-variable" substitution
|
|
176
|
+
* form. Upstream's slash lexer recognises `:{?name}` directly and emits
|
|
177
|
+
* `TRUE` / `FALSE` depending on whether the named variable is currently
|
|
178
|
+
* set; we re-create that pass here because the underlying scanner only
|
|
179
|
+
* handles the plain `:name`, `:'name'`, `:"name"` forms (the `:{?name}`
|
|
180
|
+
* form lives in a separate `xslashdefined` flex rule upstream). Doing it
|
|
181
|
+
* after the join keeps the rule local to the conditional-expression
|
|
182
|
+
* pipeline — every other call site of the slash scanner has its own
|
|
183
|
+
* variable-expansion needs that we don't want to disturb.
|
|
184
|
+
*/
|
|
185
|
+
const DEFINED_VAR_RE = /:\{\?([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
186
|
+
const expandDefinedVar = (text, isDefined) => text.replace(DEFINED_VAR_RE, (_match, name) => isDefined(name) ? 'TRUE' : 'FALSE');
|
|
187
|
+
const collectExpr = (ctx) => {
|
|
188
|
+
const parts = [];
|
|
189
|
+
for (;;) {
|
|
190
|
+
const arg = ctx.nextArg('normal');
|
|
191
|
+
if (arg === null)
|
|
192
|
+
break;
|
|
193
|
+
parts.push(arg);
|
|
194
|
+
}
|
|
195
|
+
return expandDefinedVar(parts.join(' '), (name) => ctx.settings.vars.has(name));
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Marker call indicating "discard the expression without evaluating it".
|
|
199
|
+
* Mirrors upstream `ignore_boolean_expression` — when we're already inside
|
|
200
|
+
* an inactive branch, `\if` / `\elif` arguments are dropped without
|
|
201
|
+
* expanding `:vars` or running backticks. We achieve this by simply NOT
|
|
202
|
+
* calling `nextArg`: the BackslashContext factory in `mainloop.ts` only
|
|
203
|
+
* invokes the slash scanner lazily on the first arg request, so leaving
|
|
204
|
+
* the queue untouched skips all expansion. The unconsumed `rawArgs` are
|
|
205
|
+
* dropped after the cmd returns.
|
|
206
|
+
*
|
|
207
|
+
* Kept as a named no-op so call sites read intent-fully ("dropExpr") and
|
|
208
|
+
* future refactors can replace the body without touching every caller.
|
|
209
|
+
*/
|
|
210
|
+
const dropExpr = () => {
|
|
211
|
+
// Intentionally empty — see the doc comment.
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Evaluate the joined expression against {@link parseBool}. Unrecognised
|
|
215
|
+
* tokens surface `unrecognized value "<tok>" for "\<cmd> expression":
|
|
216
|
+
* Boolean expected` to stderr (bare, no `psql: ERROR:` prefix — upstream
|
|
217
|
+
* `psql_error` shape) and evaluate to false. The caller is responsible for
|
|
218
|
+
* setting the stack state to `'false'` on this path. `cmdName` is the
|
|
219
|
+
* caller's command identifier (`'if'` / `'elif'`) so the diagnostic matches
|
|
220
|
+
* upstream verbatim.
|
|
221
|
+
*/
|
|
222
|
+
const evalExpr = (ctx, cmdName) => {
|
|
223
|
+
const raw = collectExpr(ctx);
|
|
224
|
+
const parsed = parseBool(raw);
|
|
225
|
+
if (parsed === null) {
|
|
226
|
+
const message = `unrecognized value "${raw}" for "\\${cmdName} expression": Boolean expected`;
|
|
227
|
+
ctx.settings.lastErrorResult = { message };
|
|
228
|
+
writeErr(`${message}\n`);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return parsed;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Marker symbol on BackslashContext.cmdName so the mainloop can recognise the
|
|
235
|
+
* cond commands without an interface-pollution argument. We instead attach the
|
|
236
|
+
* CondStack via a well-known field on the `settings` object — see {@link
|
|
237
|
+
* attachCondStack} / {@link getCondStack}.
|
|
238
|
+
*
|
|
239
|
+
* The mainloop is the sole owner of the CondStack, and it threads it onto the
|
|
240
|
+
* BackslashContext via this helper pair so command modules don't have to know
|
|
241
|
+
* about REPLContext.
|
|
242
|
+
*/
|
|
243
|
+
const COND_STACK_KEY = Symbol.for('neonctl.psql.condStack');
|
|
244
|
+
export const attachCondStack = (ctx, cond) => {
|
|
245
|
+
const settings = ctx.settings;
|
|
246
|
+
settings[COND_STACK_KEY] = cond;
|
|
247
|
+
};
|
|
248
|
+
export const getCondStack = (ctx) => {
|
|
249
|
+
const settings = ctx.settings;
|
|
250
|
+
const stack = settings[COND_STACK_KEY];
|
|
251
|
+
if (stack === undefined) {
|
|
252
|
+
throw new Error('cond stack not attached; cmd_cond commands must be dispatched via runMainLoop');
|
|
253
|
+
}
|
|
254
|
+
return stack;
|
|
255
|
+
};
|
|
256
|
+
const errResult = (ctx, message) => {
|
|
257
|
+
ctx.settings.lastErrorResult = { message };
|
|
258
|
+
// Upstream emits cond diagnostics bare via `psql_error("%s\n", ...)`: no
|
|
259
|
+
// `psql: ERROR:` prefix, no `\<cmd>:` prefix on top of the message (the
|
|
260
|
+
// message already includes it). We mirror that exactly so the regress
|
|
261
|
+
// expected output (`\elif: cannot occur after \else`) matches verbatim.
|
|
262
|
+
// The `errorWritten` flag tells the mainloop not to add its own
|
|
263
|
+
// `psql: ERROR: <msg>` fallback line.
|
|
264
|
+
writeErr(`${message}\n`);
|
|
265
|
+
return { status: 'error', errorWritten: true };
|
|
266
|
+
};
|
|
267
|
+
const okResult = () => ({ status: 'ok' });
|
|
268
|
+
/**
|
|
269
|
+
* Build an `{ status: 'ok' }` result that also asks the mainloop to truncate
|
|
270
|
+
* `queryBuf` back to `len`. Mirrors upstream `discard_query_text` — called
|
|
271
|
+
* by `\elif` / `\else` / `\endif` when the just-completed branch was
|
|
272
|
+
* INACTIVE, so the SQL text the skipped branch accumulated doesn't bleed
|
|
273
|
+
* into the surrounding statement.
|
|
274
|
+
*/
|
|
275
|
+
const truncResult = (len) => ({
|
|
276
|
+
status: 'ok',
|
|
277
|
+
truncateBufTo: len,
|
|
278
|
+
});
|
|
279
|
+
/**
|
|
280
|
+
* `true` when {@link IfState} corresponds to a branch that was skipping
|
|
281
|
+
* statements. Upstream `conditional_active` returns false for these. Used
|
|
282
|
+
* by the elif/else/endif commands to decide whether the
|
|
283
|
+
* `discard_query_text` step should fire on transition out of the branch.
|
|
284
|
+
*/
|
|
285
|
+
const isInactiveState = (state) => state === 'false' || state === 'else-false' || state === 'ignored';
|
|
286
|
+
export const cmdIf = {
|
|
287
|
+
name: 'if',
|
|
288
|
+
argMode: 'lex',
|
|
289
|
+
async run(ctx) {
|
|
290
|
+
const cond = getCondStack(ctx);
|
|
291
|
+
// Save the queryBuf state at the point the `\if` was encountered. The
|
|
292
|
+
// mainloop dispatches the cond command AFTER folding any preceding
|
|
293
|
+
// text into `queryBuf` (e.g. the "select" in `select \if true 42 ...`),
|
|
294
|
+
// so `ctx.queryBuf.length` is exactly upstream `save_query_text_state`'s
|
|
295
|
+
// snapshot. Inactive branches will accumulate scan-time SQL we'll later
|
|
296
|
+
// roll back via `discard_query_text` (the truncate-to-saved step in the
|
|
297
|
+
// matching `\elif`/`\else`/`\endif`).
|
|
298
|
+
const savedLen = ctx.queryBuf.length;
|
|
299
|
+
if (!cond.isActive()) {
|
|
300
|
+
// Suppressed by outer: push IGNORED and drop the expression WITHOUT
|
|
301
|
+
// expanding it. Upstream `ignore_boolean_expression` calls the lexer
|
|
302
|
+
// with backticks/vars disabled (psql.sql:1028 covers this with
|
|
303
|
+
// `\if false { \if \`nosuchcommand\` ... }`).
|
|
304
|
+
dropExpr();
|
|
305
|
+
cond.push('ignored', savedLen);
|
|
306
|
+
return Promise.resolve(okResult());
|
|
307
|
+
}
|
|
308
|
+
const truthy = evalExpr(ctx, 'if');
|
|
309
|
+
cond.push(truthy ? 'true' : 'false', savedLen);
|
|
310
|
+
return Promise.resolve(okResult());
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
export const cmdElif = {
|
|
314
|
+
name: 'elif',
|
|
315
|
+
argMode: 'lex',
|
|
316
|
+
async run(ctx) {
|
|
317
|
+
const cond = getCondStack(ctx);
|
|
318
|
+
const top = cond.top();
|
|
319
|
+
if (top === undefined) {
|
|
320
|
+
dropExpr();
|
|
321
|
+
return Promise.resolve(errResult(ctx, '\\elif: no matching \\if'));
|
|
322
|
+
}
|
|
323
|
+
// If the branch we're leaving was INACTIVE, anything its body added to
|
|
324
|
+
// `queryBuf` is scan-time accumulation we don't want — discard back to
|
|
325
|
+
// the snapshot captured at the matching `\if`/`\elif`. The truncate is
|
|
326
|
+
// applied by the mainloop; we just report the target length.
|
|
327
|
+
const wasInactive = isInactiveState(top.state);
|
|
328
|
+
const savedLen = top.savedQueryBufLen;
|
|
329
|
+
switch (top.state) {
|
|
330
|
+
case 'true': {
|
|
331
|
+
// Branch already taken — flip to IGNORED. Drop the expression
|
|
332
|
+
// without expansion (regress suite: `\if true \elif \`bad\` ...`).
|
|
333
|
+
dropExpr();
|
|
334
|
+
cond.setState('ignored');
|
|
335
|
+
// Active branch — keep buffer text. Re-anchor savedQueryBufLen
|
|
336
|
+
// to the start of the new (ignored) branch so a later `\else`/
|
|
337
|
+
// `\endif` discards just this branch's additions.
|
|
338
|
+
cond.setSavedQueryBufLen(ctx.queryBuf.length);
|
|
339
|
+
return Promise.resolve(okResult());
|
|
340
|
+
}
|
|
341
|
+
case 'false': {
|
|
342
|
+
// Have not yet found a true branch — evaluate this one.
|
|
343
|
+
// evalExpr emits its own `unrecognized value` diagnostic on failure
|
|
344
|
+
// and falls through to false, mirroring upstream.
|
|
345
|
+
const truthy = evalExpr(ctx, 'elif');
|
|
346
|
+
cond.setState(truthy ? 'true' : 'false');
|
|
347
|
+
// Apply the discard (was INACTIVE) and re-anchor at the rolled-back
|
|
348
|
+
// length so the new branch's bookkeeping starts fresh.
|
|
349
|
+
cond.setSavedQueryBufLen(savedLen);
|
|
350
|
+
return wasInactive
|
|
351
|
+
? Promise.resolve(truncResult(savedLen))
|
|
352
|
+
: Promise.resolve(okResult());
|
|
353
|
+
}
|
|
354
|
+
case 'ignored': {
|
|
355
|
+
// Outer is suppressed — stay ignored, drop args without expanding.
|
|
356
|
+
// Still discard accumulated buffer text and re-anchor.
|
|
357
|
+
dropExpr();
|
|
358
|
+
cond.setSavedQueryBufLen(savedLen);
|
|
359
|
+
return wasInactive
|
|
360
|
+
? Promise.resolve(truncResult(savedLen))
|
|
361
|
+
: Promise.resolve(okResult());
|
|
362
|
+
}
|
|
363
|
+
case 'else-true':
|
|
364
|
+
case 'else-false':
|
|
365
|
+
dropExpr();
|
|
366
|
+
return Promise.resolve(errResult(ctx, '\\elif: cannot occur after \\else'));
|
|
367
|
+
case 'none':
|
|
368
|
+
dropExpr();
|
|
369
|
+
return Promise.resolve(errResult(ctx, '\\elif: no matching \\if'));
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
export const cmdElse = {
|
|
374
|
+
name: 'else',
|
|
375
|
+
argMode: 'lex',
|
|
376
|
+
async run(ctx) {
|
|
377
|
+
const cond = getCondStack(ctx);
|
|
378
|
+
const top = cond.top();
|
|
379
|
+
if (top === undefined) {
|
|
380
|
+
return Promise.resolve(errResult(ctx, '\\else: no matching \\if'));
|
|
381
|
+
}
|
|
382
|
+
const wasInactive = isInactiveState(top.state);
|
|
383
|
+
const savedLen = top.savedQueryBufLen;
|
|
384
|
+
switch (top.state) {
|
|
385
|
+
case 'true':
|
|
386
|
+
cond.setState('else-false');
|
|
387
|
+
// Was ACTIVE — keep buffer text. Re-anchor at the current length so
|
|
388
|
+
// the upcoming else-false branch's additions can be discarded by
|
|
389
|
+
// the matching `\endif`.
|
|
390
|
+
cond.setSavedQueryBufLen(ctx.queryBuf.length);
|
|
391
|
+
return Promise.resolve(okResult());
|
|
392
|
+
case 'false':
|
|
393
|
+
cond.setState('else-true');
|
|
394
|
+
cond.setSavedQueryBufLen(savedLen);
|
|
395
|
+
return wasInactive
|
|
396
|
+
? Promise.resolve(truncResult(savedLen))
|
|
397
|
+
: Promise.resolve(okResult());
|
|
398
|
+
case 'ignored':
|
|
399
|
+
cond.setState('else-false');
|
|
400
|
+
cond.setSavedQueryBufLen(savedLen);
|
|
401
|
+
return wasInactive
|
|
402
|
+
? Promise.resolve(truncResult(savedLen))
|
|
403
|
+
: Promise.resolve(okResult());
|
|
404
|
+
case 'else-true':
|
|
405
|
+
case 'else-false':
|
|
406
|
+
return Promise.resolve(errResult(ctx, '\\else: cannot occur after \\else'));
|
|
407
|
+
case 'none':
|
|
408
|
+
return Promise.resolve(errResult(ctx, '\\else: no matching \\if'));
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
export const cmdEndif = {
|
|
413
|
+
name: 'endif',
|
|
414
|
+
argMode: 'lex',
|
|
415
|
+
async run(ctx) {
|
|
416
|
+
const cond = getCondStack(ctx);
|
|
417
|
+
const top = cond.top();
|
|
418
|
+
if (top === undefined) {
|
|
419
|
+
return Promise.resolve(errResult(ctx, '\\endif: no matching \\if'));
|
|
420
|
+
}
|
|
421
|
+
const wasInactive = isInactiveState(top.state);
|
|
422
|
+
const savedLen = top.savedQueryBufLen;
|
|
423
|
+
cond.pop();
|
|
424
|
+
return wasInactive
|
|
425
|
+
? Promise.resolve(truncResult(savedLen))
|
|
426
|
+
: Promise.resolve(okResult());
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
/** Names of the conditional commands — the mainloop dispatches these
|
|
430
|
+
* unconditionally (i.e. ignoring `cond.isActive()`) so an `\if false` block
|
|
431
|
+
* can still be closed by `\endif`. */
|
|
432
|
+
export const COND_COMMAND_NAMES = new Set([
|
|
433
|
+
'if',
|
|
434
|
+
'elif',
|
|
435
|
+
'else',
|
|
436
|
+
'endif',
|
|
437
|
+
]);
|