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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection-driven psql variable sync.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of `SyncVariables()` in upstream PostgreSQL's
|
|
5
|
+
* `src/bin/psql/command.c`. After every successful connection (the initial
|
|
6
|
+
* connect in `runPsql` and each `\c` reconnect), psql refreshes the set of
|
|
7
|
+
* read-only "connection" variables that scripts interpolate with `:DBNAME`,
|
|
8
|
+
* `:USER`, etc. Without this the names interpolate to their literal text
|
|
9
|
+
* (`:USER` → `:USER`) instead of the live connection facts.
|
|
10
|
+
*
|
|
11
|
+
* The variables synced here mirror upstream's `SyncVariables()`:
|
|
12
|
+
*
|
|
13
|
+
* - `DBNAME` — connection database
|
|
14
|
+
* - `USER` — connection user
|
|
15
|
+
* - `HOST` — host the client connected to (a socket directory
|
|
16
|
+
* for a Unix-domain socket — starts with `/`)
|
|
17
|
+
* - `PORT` — connection port (stringified)
|
|
18
|
+
* - `ENCODING` — client encoding name (the `client_encoding`
|
|
19
|
+
* ParameterStatus). The mainloop keeps this in
|
|
20
|
+
* sync on later `SET client_encoding`; we seed it
|
|
21
|
+
* here so it is correct from the first prompt.
|
|
22
|
+
* - `SERVER_VERSION_NAME`— the `server_version` ParameterStatus (e.g. `18.4`)
|
|
23
|
+
* - `SERVER_VERSION_NUM` — the numeric server version (e.g. `180004`),
|
|
24
|
+
* stringified from `Connection.serverVersion`
|
|
25
|
+
*
|
|
26
|
+
* The CLIENT version variables (`VERSION` / `VERSION_NAME` / `VERSION_NUM`)
|
|
27
|
+
* are constant for the life of the process and set once at startup — see
|
|
28
|
+
* {@link setStartupVars}.
|
|
29
|
+
*/
|
|
30
|
+
const asString = (value) => typeof value === 'string' ? value : undefined;
|
|
31
|
+
const asPort = (value) => typeof value === 'number' && Number.isFinite(value)
|
|
32
|
+
? String(value)
|
|
33
|
+
: undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Refresh the connection variables (`DBNAME`, `USER`, `HOST`, `PORT`,
|
|
36
|
+
* `ENCODING`, `SERVER_VERSION_NAME`, `SERVER_VERSION_NUM`) from the live
|
|
37
|
+
* connection. Mirrors upstream `SyncVariables()`. Call after a successful
|
|
38
|
+
* initial connect and after each `\c` reconnect.
|
|
39
|
+
*
|
|
40
|
+
* A variable is only set when the connection actually surfaces a value; this
|
|
41
|
+
* leaves any user-set value untouched if the connection cannot report one.
|
|
42
|
+
*/
|
|
43
|
+
export const syncConnectionVars = (vars, conn) => {
|
|
44
|
+
const target = conn;
|
|
45
|
+
const database = asString(target.database);
|
|
46
|
+
if (database !== undefined)
|
|
47
|
+
vars.set('DBNAME', database);
|
|
48
|
+
const user = asString(target.user);
|
|
49
|
+
if (user !== undefined)
|
|
50
|
+
vars.set('USER', user);
|
|
51
|
+
const host = asString(target.host);
|
|
52
|
+
if (host !== undefined)
|
|
53
|
+
vars.set('HOST', host);
|
|
54
|
+
const port = asPort(target.port);
|
|
55
|
+
if (port !== undefined)
|
|
56
|
+
vars.set('PORT', port);
|
|
57
|
+
// ENCODING tracks the server's `client_encoding` ParameterStatus. The
|
|
58
|
+
// mainloop refreshes it after each `SET client_encoding`; seed it here so
|
|
59
|
+
// it is populated from the very first prompt (and re-seeded after `\c`).
|
|
60
|
+
const encoding = conn.parameterStatus('client_encoding');
|
|
61
|
+
if (encoding !== undefined)
|
|
62
|
+
vars.set('ENCODING', encoding);
|
|
63
|
+
const serverVersionName = conn.parameterStatus('server_version');
|
|
64
|
+
if (serverVersionName !== undefined) {
|
|
65
|
+
vars.set('SERVER_VERSION_NAME', serverVersionName);
|
|
66
|
+
}
|
|
67
|
+
// `Connection.serverVersion` is the libpq-style integer (e.g. 180004 for
|
|
68
|
+
// 18.4); 0 means "not yet reported" — skip it so we don't write a bogus 0.
|
|
69
|
+
if (conn.serverVersion > 0) {
|
|
70
|
+
vars.set('SERVER_VERSION_NUM', String(conn.serverVersion));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Set the constant CLIENT version variables once at startup, mirroring how
|
|
75
|
+
* upstream psql seeds `VERSION` / `VERSION_NAME` / `VERSION_NUM` from its
|
|
76
|
+
* compiled-in `PG_VERSION` / `PG_VERSION_NUM`:
|
|
77
|
+
*
|
|
78
|
+
* - `VERSION` — the full banner string (upstream: `PostgreSQL <ver> …`).
|
|
79
|
+
* - `VERSION_NAME` — the bare version number (upstream: e.g. `18.4`).
|
|
80
|
+
* - `VERSION_NUM` — the numeric version (upstream: e.g. `180004`).
|
|
81
|
+
*
|
|
82
|
+
* The embedded psql is not a real PostgreSQL build, so there is no
|
|
83
|
+
* `PG_VERSION` to read. We derive the values from neonctl's own package
|
|
84
|
+
* version (passed in as {@link clientVersion}, e.g. `2.22.0`) so the client
|
|
85
|
+
* identifier is real and traceable to the shipped binary, while keeping
|
|
86
|
+
* upstream's variable *shapes*:
|
|
87
|
+
*
|
|
88
|
+
* - `VERSION` → `psql-ts (neonctl) <clientVersion>` — a banner that
|
|
89
|
+
* names the implementation so users can tell they are on the embedded
|
|
90
|
+
* TS port, mirroring the startup banner's `psql-ts (neonctl, …)` shape.
|
|
91
|
+
* - `VERSION_NAME` → `<clientVersion>` (e.g. `2.22.0`).
|
|
92
|
+
* - `VERSION_NUM` → the same version mapped into PG's NNMMPP integer form
|
|
93
|
+
* (`2.22.0` → `22200`) via {@link clientVersionNum}, so a script doing a
|
|
94
|
+
* numeric `:VERSION_NUM` comparison gets a monotonic integer.
|
|
95
|
+
*/
|
|
96
|
+
export const setStartupVars = (vars, clientVersion) => {
|
|
97
|
+
vars.set('VERSION', `psql-ts (neonctl) ${clientVersion}`);
|
|
98
|
+
vars.set('VERSION_NAME', clientVersion);
|
|
99
|
+
vars.set('VERSION_NUM', String(clientVersionNum(clientVersion)));
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Map a `MAJOR.MINOR.PATCH` semver-style version string into PG's
|
|
103
|
+
* `PG_VERSION_NUM` integer layout (`MAJOR * 10000 + MINOR * 100 + PATCH`,
|
|
104
|
+
* e.g. `2.22.0` → `22200`). Missing components default to 0; a non-numeric
|
|
105
|
+
* leading component yields `0`. Kept deliberately tolerant — the value only
|
|
106
|
+
* needs to be a monotonic integer for `:VERSION_NUM` comparisons.
|
|
107
|
+
*/
|
|
108
|
+
export const clientVersionNum = (version) => {
|
|
109
|
+
const m = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version.trim());
|
|
110
|
+
if (!m)
|
|
111
|
+
return 0;
|
|
112
|
+
const major = parseInt(m[1], 10);
|
|
113
|
+
const minor = m[2] !== undefined ? parseInt(m[2], 10) : 0;
|
|
114
|
+
const patch = m[3] !== undefined ? parseInt(m[3], 10) : 0;
|
|
115
|
+
return major * 10000 + minor * 100 + patch;
|
|
116
|
+
};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* psql Variables store.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript port of PostgreSQL's `src/bin/psql/variables.c`. Models a psql
|
|
5
|
+
* "variable space" — a name → string-value mapping with per-variable
|
|
6
|
+
* notification hooks and the parsing helpers psql uses to coerce values to
|
|
7
|
+
* booleans, tri-state ("on"/"off"/"auto") values, and integers.
|
|
8
|
+
*
|
|
9
|
+
* Deviations from upstream that are intentional:
|
|
10
|
+
*
|
|
11
|
+
* - Storage is a `Map<string, string>` rather than a doubly-linked list.
|
|
12
|
+
* Insertion order is preserved by Map (psql sorts alphabetically purely for
|
|
13
|
+
* pretty-printing in `\set`; that responsibility belongs to a future
|
|
14
|
+
* printer/help WP, not the store).
|
|
15
|
+
* - Multiple hooks per name are allowed (upstream has at most one substitute
|
|
16
|
+
* + one assign hook per variable). All hooks must return `true` for a
|
|
17
|
+
* `set()` to be accepted; if any vetoes, the value is left unchanged.
|
|
18
|
+
* - On `addHook()` we synchronously replay the current value (or `null` if
|
|
19
|
+
* unset) through the new hook. This matches the upstream behaviour where
|
|
20
|
+
* `SetVariableHooks` fires the substitute and assign hooks immediately so
|
|
21
|
+
* derived psql state can sync.
|
|
22
|
+
* - On `unset()` registered hooks are notified with `null` and remain
|
|
23
|
+
* registered. The value is removed from the map (so `has()` returns
|
|
24
|
+
* `false`), but a later `set()` will still consult the hooks.
|
|
25
|
+
* - Variable names are validated against `[A-Za-z_][A-Za-z0-9_]*` per the
|
|
26
|
+
* WP-06 spec. Upstream additionally accepts non-ASCII bytes and a leading
|
|
27
|
+
* digit; we deliberately tighten the rule for the TS port.
|
|
28
|
+
*/
|
|
29
|
+
const VALID_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
30
|
+
/** Case-insensitive prefix match: does `value` start with `prefix`? */
|
|
31
|
+
const isPrefixOf = (value, prefix) => value.length > 0 &&
|
|
32
|
+
value.length <= prefix.length &&
|
|
33
|
+
prefix.slice(0, value.length).toLowerCase() === value.toLowerCase();
|
|
34
|
+
/**
|
|
35
|
+
* Parse a string the way psql's `ParseVariableBool` does.
|
|
36
|
+
*
|
|
37
|
+
* Recognised tokens (case-insensitive, with unique-prefix matching for the
|
|
38
|
+
* word forms): `true`, `false`, `yes`, `no`, `on`, `off`, `1`, `0`. For `on`
|
|
39
|
+
* and `off` we require at least two characters of input — a bare `o` is
|
|
40
|
+
* ambiguous and upstream rejects it.
|
|
41
|
+
*
|
|
42
|
+
* Returns the parsed boolean, or `null` if the string is not recognised.
|
|
43
|
+
*/
|
|
44
|
+
const parseBool = (value) => {
|
|
45
|
+
if (value.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
if (isPrefixOf(value, 'true'))
|
|
48
|
+
return true;
|
|
49
|
+
if (isPrefixOf(value, 'false'))
|
|
50
|
+
return false;
|
|
51
|
+
if (isPrefixOf(value, 'yes'))
|
|
52
|
+
return true;
|
|
53
|
+
if (isPrefixOf(value, 'no'))
|
|
54
|
+
return false;
|
|
55
|
+
// 'on'/'off' need at least 2 chars; 'o' alone is ambiguous.
|
|
56
|
+
if (value.length >= 2) {
|
|
57
|
+
const lower = value.toLowerCase();
|
|
58
|
+
if ('on'.startsWith(lower))
|
|
59
|
+
return true;
|
|
60
|
+
if ('off'.startsWith(lower))
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (value === '1')
|
|
64
|
+
return true;
|
|
65
|
+
if (value === '0')
|
|
66
|
+
return false;
|
|
67
|
+
// WP-06 extension: any other strtol-parsable integer is truthy if non-zero,
|
|
68
|
+
// falsy if zero. Upstream `ParseVariableBool` rejects "42" outright; we
|
|
69
|
+
// accept it so callers don't need a separate code path for numeric flags.
|
|
70
|
+
const asNum = parseInt32(value);
|
|
71
|
+
if (asNum !== null)
|
|
72
|
+
return asNum !== 0;
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Parse a string as an integer the way psql's `ParseVariableNum` does
|
|
77
|
+
* (base 0, i.e. `0x` and leading-zero octal forms are accepted), and clamp
|
|
78
|
+
* to the 32-bit signed range that psql uses (the C cast `numval == (int)
|
|
79
|
+
* numval` check).
|
|
80
|
+
*
|
|
81
|
+
* Returns the integer, or `null` on syntax / range failure.
|
|
82
|
+
*/
|
|
83
|
+
const parseInt32 = (value) => {
|
|
84
|
+
if (value.length === 0)
|
|
85
|
+
return null;
|
|
86
|
+
const trimmed = value.trim();
|
|
87
|
+
if (trimmed.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
// Match the prefixes strtol(_, _, 0) accepts and pick the matching radix
|
|
90
|
+
// explicitly so we can validate the entire string (Number()/parseInt with
|
|
91
|
+
// base 0 are not portable enough for this).
|
|
92
|
+
let body = trimmed;
|
|
93
|
+
let sign = 1;
|
|
94
|
+
if (body.startsWith('+')) {
|
|
95
|
+
body = body.slice(1);
|
|
96
|
+
}
|
|
97
|
+
else if (body.startsWith('-')) {
|
|
98
|
+
sign = -1;
|
|
99
|
+
body = body.slice(1);
|
|
100
|
+
}
|
|
101
|
+
if (body.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
let radix = 10;
|
|
104
|
+
if (body.startsWith('0x') || body.startsWith('0X')) {
|
|
105
|
+
radix = 16;
|
|
106
|
+
body = body.slice(2);
|
|
107
|
+
}
|
|
108
|
+
else if (body.startsWith('0o') || body.startsWith('0O')) {
|
|
109
|
+
radix = 8;
|
|
110
|
+
body = body.slice(2);
|
|
111
|
+
}
|
|
112
|
+
else if (body.length > 1 && body.startsWith('0')) {
|
|
113
|
+
// C strtol with base 0 treats a leading 0 as octal. JS users typically
|
|
114
|
+
// expect decimal; we follow upstream for behavioural fidelity.
|
|
115
|
+
radix = 8;
|
|
116
|
+
body = body.slice(1);
|
|
117
|
+
}
|
|
118
|
+
if (body.length === 0)
|
|
119
|
+
return null;
|
|
120
|
+
const digitRe = radix === 16 ? /^[0-9a-fA-F]+$/ : radix === 8 ? /^[0-7]+$/ : /^[0-9]+$/;
|
|
121
|
+
if (!digitRe.test(body))
|
|
122
|
+
return null;
|
|
123
|
+
const parsed = sign * parseInt(body, radix);
|
|
124
|
+
if (!Number.isFinite(parsed))
|
|
125
|
+
return null;
|
|
126
|
+
// Match `numval == (int) numval` — 32-bit signed range.
|
|
127
|
+
if (parsed < -0x80000000 || parsed > 0x7fffffff)
|
|
128
|
+
return null;
|
|
129
|
+
return parsed;
|
|
130
|
+
};
|
|
131
|
+
export class VarStore {
|
|
132
|
+
constructor() {
|
|
133
|
+
this.values = new Map();
|
|
134
|
+
this.hooks = new Map();
|
|
135
|
+
}
|
|
136
|
+
set(name, value) {
|
|
137
|
+
return this.trySet(name, value).ok;
|
|
138
|
+
}
|
|
139
|
+
trySet(name, value) {
|
|
140
|
+
if (!VALID_NAME_RE.test(name)) {
|
|
141
|
+
return { ok: false, reason: 'invalid-name' };
|
|
142
|
+
}
|
|
143
|
+
const hooks = this.hooks.get(name);
|
|
144
|
+
let toStore = value;
|
|
145
|
+
if (hooks) {
|
|
146
|
+
// All hooks must accept the value. Each hook can either:
|
|
147
|
+
// - return `true` to accept as-is,
|
|
148
|
+
// - return `false` to reject silently (the prior value is kept),
|
|
149
|
+
// - return a `string` to reject with that error message
|
|
150
|
+
// (cmdSet renders it with the `psql: ` prefix), or
|
|
151
|
+
// - return `{ substitute: '<value>' }` to rewrite the stored value
|
|
152
|
+
// before subsequent hooks see it.
|
|
153
|
+
//
|
|
154
|
+
// The substitute return is the collapsed equivalent of upstream's
|
|
155
|
+
// separate substitute/assign hook pair (see
|
|
156
|
+
// `bool_substitute_hook` + `bool_assign_hook` in `command.c`).
|
|
157
|
+
// Hooks are responsible for ensuring their substituted value passes
|
|
158
|
+
// their own validation — we do NOT re-run a hook against its own
|
|
159
|
+
// substitution.
|
|
160
|
+
for (const hook of hooks) {
|
|
161
|
+
const result = hook(toStore);
|
|
162
|
+
if (result === false) {
|
|
163
|
+
return { ok: false, reason: 'hook-veto' };
|
|
164
|
+
}
|
|
165
|
+
if (typeof result === 'string') {
|
|
166
|
+
return { ok: false, reason: 'hook-veto', error: result };
|
|
167
|
+
}
|
|
168
|
+
if (typeof result === 'object' && result !== null) {
|
|
169
|
+
toStore = result.substitute;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this.values.set(name, toStore);
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
176
|
+
get(name) {
|
|
177
|
+
return this.values.get(name);
|
|
178
|
+
}
|
|
179
|
+
unset(name) {
|
|
180
|
+
const had = this.values.delete(name);
|
|
181
|
+
const hooks = this.hooks.get(name);
|
|
182
|
+
if (hooks) {
|
|
183
|
+
// Notify hooks of deletion so they can clear derived state.
|
|
184
|
+
// Upstream substitute hooks (e.g. `on_error_rollback_substitute_hook`,
|
|
185
|
+
// `bool_substitute_hook`) re-inject a default when `newval == NULL` —
|
|
186
|
+
// so `\unset ON_ERROR_ROLLBACK` actually re-stores "off",
|
|
187
|
+
// `\unset AUTOCOMMIT` re-stores "on", etc. Honor the substitute by
|
|
188
|
+
// re-storing the value the hook returns. Plain `true` / `false` /
|
|
189
|
+
// error-string returns mean "no substitute" and the slot stays empty.
|
|
190
|
+
let substituted = null;
|
|
191
|
+
for (const hook of hooks) {
|
|
192
|
+
const r = hook(null);
|
|
193
|
+
if (typeof r === 'object' && r !== null && 'substitute' in r) {
|
|
194
|
+
substituted = r.substitute;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (substituted !== null) {
|
|
198
|
+
this.values.set(name, substituted);
|
|
199
|
+
// Re-notify hooks with the substituted value so derived state
|
|
200
|
+
// (settings.onErrorRollback, etc.) gets the correct default.
|
|
201
|
+
for (const hook of hooks)
|
|
202
|
+
hook(substituted);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return had;
|
|
206
|
+
}
|
|
207
|
+
has(name) {
|
|
208
|
+
return this.values.has(name);
|
|
209
|
+
}
|
|
210
|
+
hasSubstituteHook(name) {
|
|
211
|
+
const hooks = this.hooks.get(name);
|
|
212
|
+
return hooks !== undefined && hooks.length > 0;
|
|
213
|
+
}
|
|
214
|
+
addHook(name, hook) {
|
|
215
|
+
if (!VALID_NAME_RE.test(name))
|
|
216
|
+
return;
|
|
217
|
+
const existing = this.hooks.get(name);
|
|
218
|
+
if (existing) {
|
|
219
|
+
existing.push(hook);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.hooks.set(name, [hook]);
|
|
223
|
+
}
|
|
224
|
+
// Replay the current value so the hook can sync immediately, matching
|
|
225
|
+
// upstream `SetVariableHooks`:
|
|
226
|
+
// if (shook) current->value = (*shook)(current->value);
|
|
227
|
+
// if (ahook) (void) (*ahook)(current->value);
|
|
228
|
+
// Our hooks combine substitute + assign in a single callback. If the
|
|
229
|
+
// initial replay returns a `{substitute}` (the bool_substitute_hook /
|
|
230
|
+
// verbosity_substitute_hook etc. pattern), persist that and re-run the
|
|
231
|
+
// hook so derived state (settings.verbosity, settings.echo, ...) syncs
|
|
232
|
+
// to the substituted value. This is how upstream seeds defaults like
|
|
233
|
+
// `ON_ERROR_STOP=off`, `VERBOSITY=default`, `QUIET=off` etc. simply
|
|
234
|
+
// from `SetVariableHooks` — no explicit `SetVariable("…", "off")` call
|
|
235
|
+
// is needed for variables whose default matches the unset substitute.
|
|
236
|
+
const current = this.values.get(name);
|
|
237
|
+
const result = hook(current ?? null);
|
|
238
|
+
if (typeof result === 'object' &&
|
|
239
|
+
result !== null &&
|
|
240
|
+
'substitute' in result) {
|
|
241
|
+
this.values.set(name, result.substitute);
|
|
242
|
+
hook(result.substitute);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
entries() {
|
|
246
|
+
return this.values.entries();
|
|
247
|
+
}
|
|
248
|
+
asBool(name, defaultValue = false) {
|
|
249
|
+
const value = this.values.get(name);
|
|
250
|
+
if (value === undefined)
|
|
251
|
+
return defaultValue;
|
|
252
|
+
const parsed = parseBool(value);
|
|
253
|
+
return parsed ?? defaultValue;
|
|
254
|
+
}
|
|
255
|
+
asTriple(name, defaultValue) {
|
|
256
|
+
const value = this.values.get(name);
|
|
257
|
+
if (value === undefined)
|
|
258
|
+
return defaultValue;
|
|
259
|
+
// "auto" is matched first as a unique prefix, so "a", "au", "aut",
|
|
260
|
+
// "auto" all map to 'auto'. psql's actual call site does an
|
|
261
|
+
// `pg_strncasecmp(value, "auto", len)` before falling through to
|
|
262
|
+
// ParseVariableBool — we do the same.
|
|
263
|
+
if (isPrefixOf(value, 'auto'))
|
|
264
|
+
return 'auto';
|
|
265
|
+
const parsed = parseBool(value);
|
|
266
|
+
if (parsed === null) {
|
|
267
|
+
return {
|
|
268
|
+
error: `unrecognized value "${value}" for "${name}": Boolean expected`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return parsed ? 'on' : 'off';
|
|
272
|
+
}
|
|
273
|
+
asInt(name, defaultValue = 0) {
|
|
274
|
+
const value = this.values.get(name);
|
|
275
|
+
if (value === undefined)
|
|
276
|
+
return defaultValue;
|
|
277
|
+
const parsed = parseInt32(value);
|
|
278
|
+
if (parsed === null) {
|
|
279
|
+
return {
|
|
280
|
+
error: `invalid value "${value}" for "${name}": integer expected`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return parsed;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** Factory mirroring the upstream `CreateVariableSpace()` entry point. */
|
|
287
|
+
export const createVarStore = () => new VarStore();
|