@mason-data/cli 0.8.0 → 0.10.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 +19 -8
- package/dist/src/api/error.js +4 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +166 -32
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/proxy.js +129 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +527 -103
- package/dist/src/api/postgres/translate.js +47 -2
- package/dist/src/cli/background.js +18 -19
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/command.js +337 -15
- package/dist/src/cli/commands.js +458 -169
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +2 -2
- package/dist/src/cli/deploy.js +680 -0
- package/dist/src/cli/dev.js +148 -27
- package/dist/src/cli/doctor.js +122 -36
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +116 -16
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +154 -33
- package/dist/src/cli/login.js +292 -0
- package/dist/src/cli/machine.js +125 -40
- package/dist/src/cli/main.js +59 -10
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +17 -14
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +1 -1
- package/dist/src/cli/pin.js +6 -2
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +130 -39
- package/dist/src/cli/project.js +100 -1
- package/dist/src/cli/readiness.js +42 -6
- package/dist/src/cli/registry.js +89 -36
- package/dist/src/cli/replica-hosted.js +1324 -0
- package/dist/src/cli/replica.js +114 -0
- package/dist/src/cli/replication-mode.js +5 -0
- package/dist/src/cli/request-filter.js +16 -6
- package/dist/src/cli/roles.js +2 -1
- package/dist/src/cli/run-server.js +179 -9
- package/dist/src/cli/secret-input.js +193 -0
- package/dist/src/cli/security.js +57 -10
- package/dist/src/cli/serve.js +65 -9
- package/dist/src/cli/share-run.js +51 -8
- package/dist/src/cli/share.js +23 -18
- package/dist/src/cli/source-edit.js +10 -4
- package/dist/src/cli/source-role.js +343 -0
- package/dist/src/cli/sources.js +50 -23
- package/dist/src/cli/style.js +16 -1
- package/dist/src/cli/supervise.js +15 -23
- package/dist/src/cli/sync.js +17 -8
- package/dist/src/cli/token-hosted.js +343 -0
- package/dist/src/cli/tunnel.js +1 -1
- package/dist/src/cli/ui-hosted.js +163 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +162 -17
- package/dist/src/cli/usage.js +19 -15
- package/dist/src/cli/user-hosted.js +364 -0
- package/dist/src/cli/user.js +275 -105
- package/dist/src/demo/seed.js +26 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +39 -8
- package/dist/src/duck/executor-worker.js +2 -2
- package/dist/src/duck/executor.js +6 -5
- package/dist/src/duck/explain.js +4 -0
- package/dist/src/duck/functions.js +448 -49
- package/dist/src/duck/gaps.js +34 -16
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +1 -1
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +4 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +9 -7
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +275 -30
- package/dist/src/duck/publication.js +14 -0
- package/dist/src/duck/requests.js +9 -5
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4567 -583
- package/dist/src/duck/row-filter.js +12 -6
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +294 -68
- package/dist/src/duck/stream.js +9 -5
- package/dist/src/duck/sync.js +602 -126
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +63 -7
- package/dist/src/duck/usage.js +6 -4
- package/dist/src/duck/users.js +110 -9
- package/dist/src/duck/verified.js +159 -2
- package/dist/src/duck/views.js +4 -3
- package/dist/src/lib/cidr.js +177 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +13 -4
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +387 -49
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +84 -19
- package/dist/src/web/app.js +9 -4
- package/dist/src/web/pages/status.js +178 -13
- package/dist/src/web/pages/syncs.js +1 -1
- package/dist/src/web/pages/tables.js +8 -3
- package/dist/src/web/pages/users.js +4 -3
- package/dist/src/web/shared.js +19 -3
- package/package.json +8 -7
|
@@ -31,14 +31,29 @@ export class NumericError extends Error {
|
|
|
31
31
|
sqlstate;
|
|
32
32
|
/** the registry entry that refused it (src/duck/gaps.ts): the request log's `gap`; null for Postgres's own error */
|
|
33
33
|
gap;
|
|
34
|
-
|
|
34
|
+
/** Postgres's DETAIL, apart from its message, as Postgres sends them (`numeric field overflow` and its field's limit) */
|
|
35
|
+
detail;
|
|
36
|
+
constructor(message, sqlstate, gap = null, detail = null) { super(message); this.sqlstate = sqlstate; this.gap = gap; this.detail = detail; }
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A NUMERIC FIELD OVERFLOW IN POSTGRES'S WORDS (22003), the one place they are written: its message, and its DETAIL as
|
|
40
|
+
* `apply_typmod` gives it, for every path that raises one (this module's typmod, the engine's DECIMAL cast in
|
|
41
|
+
* src/duck/store.ts `postgresWording`). An infinite value has its own DETAIL (Postgres 14+).
|
|
42
|
+
*/
|
|
43
|
+
export function fieldOverflow(precision, scale, infinite = false) {
|
|
44
|
+
const digits = precision - scale;
|
|
45
|
+
return { message: "numeric field overflow", detail: infinite ? `A field with precision ${precision}, scale ${scale} cannot hold an infinite value.`
|
|
46
|
+
: `A field with precision ${precision}, scale ${scale} must round to an absolute value less than ${digits === 0 ? "1" : `10^${digits}`}.` };
|
|
35
47
|
}
|
|
36
48
|
/** Postgres's limits on a numeric's scale (numeric.h): a quotient's scale is held between these */
|
|
37
49
|
const NUMERIC_MIN_SIG_DIGITS = 16;
|
|
38
50
|
const NUMERIC_MAX_DISPLAY_SCALE = 1000;
|
|
39
51
|
/** the decimal digits in one base-10000 group (numeric.c's DEC_DIGITS, NBASE = 10^DEC_DIGITS) */
|
|
40
52
|
const DEC_DIGITS = 4;
|
|
41
|
-
|
|
53
|
+
/** 10^n for the scales a value meets most, computed once (issue 665: a GROUP BY's key evaluates a program for every
|
|
54
|
+
* row, and each division asks for several) */
|
|
55
|
+
const POW10 = Array.from({ length: 64 }, (_, i) => 10n ** BigInt(i));
|
|
56
|
+
const pow10 = (n) => POW10[n] ?? 10n ** BigInt(n);
|
|
42
57
|
const abs = (x) => x < 0n ? -x : x;
|
|
43
58
|
/** `x / d` rounded half away from zero (d > 0): how numeric.c's `round_var` and `div_var(…, round = true)` round */
|
|
44
59
|
function divRound(x, d) {
|
|
@@ -50,12 +65,20 @@ function divRound(x, d) {
|
|
|
50
65
|
/** digits, an underscore allowed between two of them (Postgres 16: `1_000`) */
|
|
51
66
|
const DIGITS = String.raw `\d+(?:_\d+)*`;
|
|
52
67
|
const NUMERIC_TEXT = new RegExp(String.raw `^\s*([+-])?(${DIGITS})?(?:\.(${DIGITS})?)?(?:[eE]([+-]?${DIGITS}))?\s*$`);
|
|
68
|
+
/** a sign, digits and decimals, nothing else: what `NUMERIC_TEXT` reads as the same value and scale */
|
|
69
|
+
const PLAIN = /^([+-])?(\d+)(?:\.(\d+))?$/;
|
|
53
70
|
/** Postgres 16's non-decimal integers: `0x1F`, `0o17`, `0b101`, an underscore before any digit (`0x_1F`) */
|
|
54
71
|
const NON_DECIMAL = /^\s*([+-])?0(?:[xX]((?:_?[0-9a-fA-F])+)|[oO]((?:_?[0-7])+)|[bB]((?:_?[01])+))\s*$/;
|
|
55
72
|
/** A numeric from its text, as `numeric_in` reads it (held to Postgres by tests/pg-numeric.test.ts): the scale is the
|
|
56
73
|
* decimals written, less the exponent (never below 0): `1.50` has scale 2, `1.5e3` is 1500 with scale 0, `1e-3` is
|
|
57
74
|
* 0.001 with scale 3; `1_000` is 1000 and `0x1F` is 31 (scale 0). NaN and Infinity are no PgNumeric: refused here. */
|
|
58
75
|
export function parseNumeric(text) {
|
|
76
|
+
// (the plain text DuckDB writes of a DECIMAL or an integer, `-12.50`: read without the general grammar below)
|
|
77
|
+
const plain = PLAIN.exec(text);
|
|
78
|
+
if (plain !== null) {
|
|
79
|
+
const unscaled = BigInt(plain[3] === undefined ? plain[2] : plain[2] + plain[3]);
|
|
80
|
+
return { unscaled: plain[1] === "-" ? -unscaled : unscaled, scale: plain[3]?.length ?? 0 };
|
|
81
|
+
}
|
|
59
82
|
const x = NON_DECIMAL.exec(text);
|
|
60
83
|
if (x !== null) {
|
|
61
84
|
const [prefix, digits] = x[2] !== undefined ? ["0x", x[2]] : x[3] !== undefined ? ["0o", x[3]] : ["0b", x[4]];
|
|
@@ -92,6 +115,121 @@ export function numericText(n) {
|
|
|
92
115
|
const text = n.scale === 0 ? whole : `${whole}.${digits.slice(digits.length - n.scale)}`;
|
|
93
116
|
return n.unscaled < 0n ? `-${text}` : text;
|
|
94
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* WHETHER A NUMERIC'S TEXT IS WIDER THAN DuckDB's DECIMAL(width, scale): THE ONE RULE of what a DECIMAL holds exactly,
|
|
120
|
+
* for the copy (src/duck/sync.ts: a value the stream brings past its synced column's scale re-copies the table) and for
|
|
121
|
+
* a parameter compared with a value the engine holds exactly (`fitsEngineType`). NaN and ±Infinity are wider than every
|
|
122
|
+
* DECIMAL; `significant`: only the decimals a value USES must fit (a declared scale's text has its trailing zeros,
|
|
123
|
+
* 19.990000…), else every decimal written. The 0 of 0.5 counts as an integer digit (conservative: a value said wider
|
|
124
|
+
* is only ever compared by its exact key, never rounded).
|
|
125
|
+
*/
|
|
126
|
+
export function numericWiderThan(text, scale, significant, width = 38) {
|
|
127
|
+
if (text === "NaN" || text === "Infinity" || text === "-Infinity")
|
|
128
|
+
return true;
|
|
129
|
+
const m = /^-?(\d*)(?:\.(\d+))?$/.exec(text);
|
|
130
|
+
if (m === null)
|
|
131
|
+
return true;
|
|
132
|
+
const decimals = significant ? (m[2] ?? "").replace(/0+$/, "") : m[2] ?? "";
|
|
133
|
+
return decimals.length > scale || m[1].length > width - scale;
|
|
134
|
+
}
|
|
135
|
+
/** each integer type DuckDB names (`typeof`), by its range */
|
|
136
|
+
const ENGINE_INTEGERS = {
|
|
137
|
+
TINYINT: [-(2n ** 7n), 2n ** 7n - 1n], SMALLINT: [-(2n ** 15n), 2n ** 15n - 1n], INTEGER: [-(2n ** 31n), 2n ** 31n - 1n],
|
|
138
|
+
BIGINT: [-(2n ** 63n), 2n ** 63n - 1n], HUGEINT: [-(2n ** 127n), 2n ** 127n - 1n],
|
|
139
|
+
UTINYINT: [0n, 2n ** 8n - 1n], USMALLINT: [0n, 2n ** 16n - 1n], UINTEGER: [0n, 2n ** 32n - 1n], UBIGINT: [0n, 2n ** 64n - 1n], UHUGEINT: [0n, 2n ** 128n - 1n],
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* WHETHER THE ENGINE'S TYPE HOLDS A NUMERIC EXACTLY (issue 214): `type` is DuckDB's own name of it (`typeof`, folded
|
|
143
|
+
* by DuckDB to a constant), `text` Postgres's numeric_out of a bound parameter (src/duck/analyze.ts readParameters). A
|
|
144
|
+
* DECIMAL(w, s) holds what is no wider (`numericWiderThan`, its trailing zeros dropped); an integer type a whole number
|
|
145
|
+
* within its range; anything else (a DOUBLE, a text) nothing. A value held exactly is cast into the type by DuckDB
|
|
146
|
+
* without rounding, so a comparison in it is Postgres's; any other is compared by its exact key (src/duck/rewrite.ts
|
|
147
|
+
* `numericParamComparison`).
|
|
148
|
+
*/
|
|
149
|
+
export function fitsEngineType(type, text) {
|
|
150
|
+
const decimal = /^DECIMAL\((\d+),(\d+)\)$/.exec(type);
|
|
151
|
+
if (decimal !== null)
|
|
152
|
+
return !numericWiderThan(text, Number(decimal[2]), true, Number(decimal[1]));
|
|
153
|
+
const range = ENGINE_INTEGERS[type];
|
|
154
|
+
if (range === undefined || numericWiderThan(text, 0, true, 40))
|
|
155
|
+
return false;
|
|
156
|
+
const whole = BigInt(text.replace(/\.0*$/, ""));
|
|
157
|
+
return whole >= range[0] && whole <= range[1];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A numeric's ORDER KEY (issue 252): bytes whose order is numeric_cmp's, for numeric_out's text (or `NaN`, `Infinity`,
|
|
161
|
+
* `-Infinity`): -Infinity < the negatives < 0 < the positives < Infinity < NaN, as Postgres sorts them; equal values
|
|
162
|
+
* (`2.5`, `2.50`) are one key. A finite value is its sign, its decimal exponent and its significant digits; a negative
|
|
163
|
+
* one's exponent and digits are complemented, ended by a byte above every digit, so a longer one sorts first.
|
|
164
|
+
*/
|
|
165
|
+
export function numericOrderKey(text) {
|
|
166
|
+
const t = text.trim();
|
|
167
|
+
if (/^nan$/i.test(t))
|
|
168
|
+
return "5";
|
|
169
|
+
if (/^\+?inf(inity)?$/i.test(t))
|
|
170
|
+
return "4";
|
|
171
|
+
if (/^-inf(inity)?$/i.test(t))
|
|
172
|
+
return "0";
|
|
173
|
+
const n = parseNumeric(t);
|
|
174
|
+
if (n.unscaled === 0n)
|
|
175
|
+
return "2";
|
|
176
|
+
const digits = abs(n.unscaled).toString().replace(/0+$/, "");
|
|
177
|
+
// the value is 0.<digits> × 10^exponent
|
|
178
|
+
const exponent = abs(n.unscaled).toString().length - n.scale;
|
|
179
|
+
// (numeric's exponent lies within ±200000: a fixed width of six digits after an offset keeps it in order)
|
|
180
|
+
const e = String(exponent + 500000).padStart(7, "0");
|
|
181
|
+
if (n.unscaled > 0n)
|
|
182
|
+
return `3${e}${digits}`;
|
|
183
|
+
const complement = (s) => s.replace(/\d/g, (d) => String(9 - Number(d)));
|
|
184
|
+
return `1${complement(e)}${complement(digits)}~`;
|
|
185
|
+
}
|
|
186
|
+
/** A numeric cast to an integer (`numeric_int4`, `numeric_int8`, `numeric_int2`): rounded half away from zero, or
|
|
187
|
+
* 22003 past the type's range, in Postgres's words; NaN and Infinity raise 0A000 as Postgres does */
|
|
188
|
+
export function numericToInteger(text, bits) {
|
|
189
|
+
const t = text.trim();
|
|
190
|
+
const name = bits === 16 ? "smallint" : bits === 32 ? "integer" : "bigint";
|
|
191
|
+
// (Postgres's words, which name the type and never the value)
|
|
192
|
+
if (/^nan$/i.test(t)) {
|
|
193
|
+
if (bits === 16)
|
|
194
|
+
throw new NumericError("cannot convert NaN to smallint", "0A000");
|
|
195
|
+
if (bits === 32)
|
|
196
|
+
throw new NumericError("cannot convert NaN to integer", "0A000");
|
|
197
|
+
throw new NumericError("cannot convert NaN to bigint", "0A000");
|
|
198
|
+
}
|
|
199
|
+
if (/inf/i.test(t)) {
|
|
200
|
+
if (bits === 16)
|
|
201
|
+
throw new NumericError("cannot convert infinity to smallint", "0A000");
|
|
202
|
+
if (bits === 32)
|
|
203
|
+
throw new NumericError("cannot convert infinity to integer", "0A000");
|
|
204
|
+
throw new NumericError("cannot convert infinity to bigint", "0A000");
|
|
205
|
+
}
|
|
206
|
+
const r = round(parseNumeric(t), 0).unscaled;
|
|
207
|
+
const limit = 1n << BigInt(bits - 1);
|
|
208
|
+
if (r < -limit || r >= limit)
|
|
209
|
+
throw new NumericError(`${name} out of range`, "22003");
|
|
210
|
+
return r.toString();
|
|
211
|
+
}
|
|
212
|
+
/** A numeric cast to float8 (`numeric_float8`, which is `float8in(numeric_out(x))`, issue 217): NaN and ±Infinity are
|
|
213
|
+
* themselves; any other text the double nearest it (`Number` of a decimal text rounds correctly, as `strtod` does).
|
|
214
|
+
* Where that double is ±Infinity, or 0 for a value that is not zero, 22003 in Postgres's words (`float8in_internal`),
|
|
215
|
+
* the text quoted whole; a subnormal (1e-310) is returned, as `float8in` returns it */
|
|
216
|
+
export function numericToDouble(text) {
|
|
217
|
+
const t = text.trim();
|
|
218
|
+
if (/^nan$/i.test(t))
|
|
219
|
+
return NaN;
|
|
220
|
+
if (/^\+?inf(inity)?$/i.test(t))
|
|
221
|
+
return Infinity;
|
|
222
|
+
if (/^-inf(inity)?$/i.test(t))
|
|
223
|
+
return -Infinity;
|
|
224
|
+
const d = Number(t);
|
|
225
|
+
if (Number.isNaN(d))
|
|
226
|
+
throw new NumericError(`invalid input syntax for type double precision: "${t}"`, "22P02");
|
|
227
|
+
if (!Number.isFinite(d) || (d === 0 && /[1-9]/.test(t.replace(/[eE].*$/, "")))) {
|
|
228
|
+
throw new NumericError(`"${t}" is out of range for type double precision`, "22003");
|
|
229
|
+
}
|
|
230
|
+
// (a numeric's zero is never negative: numeric_out writes `-0.00` as `0.00`, whose double is +0)
|
|
231
|
+
return d === 0 ? 0 : d;
|
|
232
|
+
}
|
|
95
233
|
/** The value at a larger scale (more trailing zeros), unchanged. */
|
|
96
234
|
const atScale = (n, scale) => n.unscaled * pow10(scale - n.scale);
|
|
97
235
|
export const add = (a, b) => {
|
|
@@ -122,15 +260,26 @@ function groups(n) {
|
|
|
122
260
|
const digits = lsFirst.slice(low).reverse();
|
|
123
261
|
return { digits, weight: lsFirst.length - 1 - fracGroups };
|
|
124
262
|
}
|
|
263
|
+
/** the weight and the value of a value's FIRST base-10000 group (`groups`' weight and digits[0]; 0 and 0 for zero), read
|
|
264
|
+
* off its decimal digits: the most significant digit's place is 10^(digits - 1 - scale), its group the one holding
|
|
265
|
+
* that place, and the group's value the digits down to the group's last place. All `select_div_scale` reads of a value */
|
|
266
|
+
function leadGroup(n) {
|
|
267
|
+
if (n.unscaled === 0n)
|
|
268
|
+
return [0, 0];
|
|
269
|
+
const digits = abs(n.unscaled).toString();
|
|
270
|
+
const place = digits.length - 1 - n.scale;
|
|
271
|
+
const weight = Math.floor(place / DEC_DIGITS);
|
|
272
|
+
const width = place - weight * DEC_DIGITS + 1;
|
|
273
|
+
// (a group past the last digit is padded with its zeros: 0.1 is the group 1000 at weight -1)
|
|
274
|
+
return [weight, Number(digits.slice(0, width).padEnd(width, "0"))];
|
|
275
|
+
}
|
|
125
276
|
/**
|
|
126
277
|
* `select_div_scale` (numeric.c), faithfully: a quotient keeps at least NUMERIC_MIN_SIG_DIGITS significant digits —
|
|
127
278
|
* the scale is read from the WEIGHTS and FIRST GROUPS of the two values, so it depends on the values, not only their
|
|
128
279
|
* types — and never fewer decimals than either input shows, between 0 and NUMERIC_MAX_DISPLAY_SCALE.
|
|
129
280
|
*/
|
|
130
281
|
export function divScale(a, b) {
|
|
131
|
-
const
|
|
132
|
-
const weight1 = ga.digits.length ? ga.weight : 0, first1 = ga.digits[0] ?? 0;
|
|
133
|
-
const weight2 = gb.digits.length ? gb.weight : 0, first2 = gb.digits[0] ?? 0;
|
|
282
|
+
const [weight1, first1] = leadGroup(a), [weight2, first2] = leadGroup(b);
|
|
134
283
|
// the quotient's weight; when the first groups are equal it may be either, and the smaller is assumed
|
|
135
284
|
let qweight = weight1 - weight2;
|
|
136
285
|
if (first1 <= first2)
|
|
@@ -242,7 +391,8 @@ export function withTypmod(a, precision, scale) {
|
|
|
242
391
|
const maxDigits = precision - scale;
|
|
243
392
|
const digits = r.unscaled === 0n ? null : abs(r.unscaled).toString().length - r.scale;
|
|
244
393
|
if (digits !== null && digits > maxDigits) {
|
|
245
|
-
|
|
394
|
+
const o = fieldOverflow(precision, scale);
|
|
395
|
+
throw new NumericError(o.message, "22003", null, o.detail);
|
|
246
396
|
}
|
|
247
397
|
return r;
|
|
248
398
|
}
|
|
@@ -337,16 +487,46 @@ export function aggregateType(agg, arg) {
|
|
|
337
487
|
case "max": return arg;
|
|
338
488
|
}
|
|
339
489
|
}
|
|
340
|
-
/**
|
|
490
|
+
/**
|
|
491
|
+
* A PROGRAM IS AN IMMUTABLE VALUE, AND A NODE MAY HAVE SEVERAL PARENTS (issue 244, #484): a FROM item's column read k
|
|
492
|
+
* times by the level above is ONE node those k references share (src/duck/rewrite.ts `carried`), never k copies, so a
|
|
493
|
+
* chain of levels each reading the one below three times is linear in its depth, not 3^depth. Nothing changes a node
|
|
494
|
+
* once it is made (a shared node changed would change every reader), and every reader here is linear in the DISTINCT
|
|
495
|
+
* nodes: `cellsOf`, `evaluate` (each node at most once a row), `mapCells` (sharing kept) and the one text a program
|
|
496
|
+
* leaves the planner in, `programJson`.
|
|
497
|
+
*/
|
|
498
|
+
/** The cells a program reads, each once, in the order a walk from its root first meets them: over its DISTINCT nodes
|
|
499
|
+
* (a node several parents share is walked once), memoised per node, since a program is never changed once made. */
|
|
341
500
|
export function cellsOf(p) {
|
|
501
|
+
const known = CELLS.get(p);
|
|
502
|
+
if (known !== undefined)
|
|
503
|
+
return known;
|
|
504
|
+
const out = new Set();
|
|
505
|
+
if (p.op === "cell")
|
|
506
|
+
out.add(p.key);
|
|
507
|
+
else
|
|
508
|
+
for (const child of childrenOf(p))
|
|
509
|
+
for (const k of cellsOf(child))
|
|
510
|
+
out.add(k);
|
|
511
|
+
const list = [...out];
|
|
512
|
+
CELLS.set(p, list);
|
|
513
|
+
return list;
|
|
514
|
+
}
|
|
515
|
+
const CELLS = new WeakMap();
|
|
516
|
+
/** a node's children, in the order its fields are written (an `index` before a pick's arms, a rescale's `scale` after its value) */
|
|
517
|
+
function childrenOf(p) {
|
|
342
518
|
switch (p.op) {
|
|
343
|
-
case "cell":
|
|
519
|
+
case "cell":
|
|
344
520
|
case "literal": return [];
|
|
345
521
|
case "add":
|
|
346
522
|
case "sub":
|
|
347
523
|
case "mul":
|
|
348
524
|
case "div":
|
|
349
|
-
case "mod":
|
|
525
|
+
case "mod":
|
|
526
|
+
case "greatest":
|
|
527
|
+
case "least":
|
|
528
|
+
case "coalesce":
|
|
529
|
+
case "stddev": return p.args;
|
|
350
530
|
case "neg":
|
|
351
531
|
case "round":
|
|
352
532
|
case "typmod":
|
|
@@ -354,45 +534,61 @@ export function cellsOf(p) {
|
|
|
354
534
|
case "abs":
|
|
355
535
|
case "floor":
|
|
356
536
|
case "ceil":
|
|
357
|
-
case "pad": return
|
|
358
|
-
case "
|
|
359
|
-
case "
|
|
360
|
-
case "coalesce":
|
|
361
|
-
case "stddev": return p.args.flatMap(cellsOf);
|
|
362
|
-
case "pick": return [...cellsOf(p.index), ...p.args.flatMap(cellsOf)];
|
|
363
|
-
case "rescale": return [...cellsOf(p.arg), ...cellsOf(p.scale)];
|
|
537
|
+
case "pad": return [p.arg];
|
|
538
|
+
case "pick": return [p.index, ...p.args];
|
|
539
|
+
case "rescale": return [p.arg, p.scale];
|
|
364
540
|
}
|
|
365
541
|
}
|
|
366
|
-
/**
|
|
367
|
-
|
|
542
|
+
/** each literal node's value, parsed once (a program is never changed once made, and no value is ever changed) */
|
|
543
|
+
const LITERALS = new WeakMap();
|
|
544
|
+
/** The value of a program over one row, or null; throws NumericError where Postgres raises (a division by zero). Each
|
|
545
|
+
* node several parents share is computed AT MOST ONCE for the row (`memo`, one a row), and only where Postgres
|
|
546
|
+
* computes it: a CASE's untaken arm and a COALESCE's arguments after the first that is not NULL never are. */
|
|
547
|
+
export function evaluate(p, row, memo = new Map()) {
|
|
548
|
+
if (memo.has(p))
|
|
549
|
+
return memo.get(p);
|
|
550
|
+
const v = evaluateNode(p, row, memo);
|
|
551
|
+
memo.set(p, v);
|
|
552
|
+
return v;
|
|
553
|
+
}
|
|
554
|
+
function evaluateNode(p, row, memo) {
|
|
555
|
+
const ev = (x) => evaluate(x, row, memo);
|
|
368
556
|
switch (p.op) {
|
|
369
557
|
case "cell": {
|
|
370
558
|
const v = row[p.key];
|
|
371
559
|
return v === null || v === undefined ? null : numericOf(v);
|
|
372
560
|
}
|
|
373
|
-
case "literal":
|
|
561
|
+
case "literal": {
|
|
562
|
+
// (a literal is the same on every row: read once, `LITERALS`)
|
|
563
|
+
let v = LITERALS.get(p);
|
|
564
|
+
if (v === undefined) {
|
|
565
|
+
v = parseNumeric(p.text);
|
|
566
|
+
LITERALS.set(p, v);
|
|
567
|
+
}
|
|
568
|
+
return v;
|
|
569
|
+
}
|
|
374
570
|
case "neg": {
|
|
375
|
-
const a =
|
|
571
|
+
const a = ev(p.arg);
|
|
376
572
|
return a === null ? null : { unscaled: -a.unscaled, scale: a.scale };
|
|
377
573
|
}
|
|
378
574
|
case "round": {
|
|
379
|
-
const a =
|
|
575
|
+
const a = ev(p.arg);
|
|
380
576
|
return a === null ? null : round(a, p.scale);
|
|
381
577
|
}
|
|
382
578
|
case "typmod": {
|
|
383
|
-
const a =
|
|
579
|
+
const a = ev(p.arg);
|
|
384
580
|
return a === null ? null : withTypmod(a, p.precision, p.scale);
|
|
385
581
|
}
|
|
386
582
|
case "trunc": {
|
|
387
|
-
const a =
|
|
583
|
+
const a = ev(p.arg);
|
|
388
584
|
return a === null ? null : trunc(a, p.scale);
|
|
389
585
|
}
|
|
390
586
|
case "pad": {
|
|
391
|
-
const a =
|
|
587
|
+
const a = ev(p.arg);
|
|
392
588
|
return a === null || a.scale >= p.scale ? a : { unscaled: a.unscaled * pow10(p.scale - a.scale), scale: p.scale };
|
|
393
589
|
}
|
|
394
590
|
case "rescale": {
|
|
395
|
-
const a =
|
|
591
|
+
const a = ev(p.arg), s = ev(p.scale);
|
|
396
592
|
if (a === null || s === null || s.scale !== 0 || s.unscaled < 0n)
|
|
397
593
|
return a;
|
|
398
594
|
const to = Number(s.unscaled);
|
|
@@ -402,20 +598,20 @@ export function evaluate(p, row) {
|
|
|
402
598
|
return a.unscaled % by === 0n ? { unscaled: a.unscaled / by, scale: to } : a;
|
|
403
599
|
}
|
|
404
600
|
case "abs": {
|
|
405
|
-
const a =
|
|
601
|
+
const a = ev(p.arg);
|
|
406
602
|
return a === null ? null : { unscaled: abs(a.unscaled), scale: a.scale };
|
|
407
603
|
}
|
|
408
604
|
case "floor": {
|
|
409
|
-
const a =
|
|
605
|
+
const a = ev(p.arg);
|
|
410
606
|
return a === null ? null : floor(a);
|
|
411
607
|
}
|
|
412
608
|
case "ceil": {
|
|
413
|
-
const a =
|
|
609
|
+
const a = ev(p.arg);
|
|
414
610
|
return a === null ? null : ceil(a);
|
|
415
611
|
}
|
|
416
612
|
case "coalesce": {
|
|
417
613
|
for (const x of p.args) {
|
|
418
|
-
const v =
|
|
614
|
+
const v = ev(x);
|
|
419
615
|
if (v !== null)
|
|
420
616
|
return v;
|
|
421
617
|
}
|
|
@@ -425,68 +621,210 @@ export function evaluate(p, row) {
|
|
|
425
621
|
case "least": {
|
|
426
622
|
let best = null;
|
|
427
623
|
for (const x of p.args) {
|
|
428
|
-
const v =
|
|
624
|
+
const v = ev(x);
|
|
429
625
|
if (v !== null && (best === null || (p.op === "greatest" ? compare(v, best) > 0 : compare(v, best) < 0)))
|
|
430
626
|
best = v;
|
|
431
627
|
}
|
|
432
628
|
return best;
|
|
433
629
|
}
|
|
434
630
|
case "mod": {
|
|
435
|
-
const a =
|
|
631
|
+
const a = ev(p.args[0]), b = ev(p.args[1]);
|
|
436
632
|
return a === null || b === null ? null : mod(a, b);
|
|
437
633
|
}
|
|
438
634
|
case "pick": {
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
return arm === undefined ? null : evaluate(arm, row);
|
|
635
|
+
const arm = takenArm(p, ev);
|
|
636
|
+
return arm === undefined ? null : ev(arm);
|
|
442
637
|
}
|
|
443
638
|
case "stddev": {
|
|
444
|
-
const [n, x, x2] = p.args.map(
|
|
639
|
+
const [n, x, x2] = p.args.map(ev);
|
|
445
640
|
return n === null || n === undefined || x === null || x === undefined || x2 === null || x2 === undefined ? null : stddev(n, x, x2, p.sample, p.variance);
|
|
446
641
|
}
|
|
447
642
|
default: {
|
|
448
|
-
const a =
|
|
643
|
+
const a = ev(p.args[0]), b = ev(p.args[1]);
|
|
449
644
|
if (a === null || b === null)
|
|
450
645
|
return null;
|
|
451
646
|
return p.op === "add" ? add(a, b) : p.op === "sub" ? sub(a, b) : p.op === "mul" ? mul(a, b) : div(a, b);
|
|
452
647
|
}
|
|
453
648
|
}
|
|
454
649
|
}
|
|
650
|
+
/** the arm of a CASE the row took, by its index's value; undefined past the arms (a CASE with no ELSE) or for a NULL index */
|
|
651
|
+
function takenArm(p, ev) {
|
|
652
|
+
const i = ev(p.index);
|
|
653
|
+
return i === null || i.scale !== 0 ? undefined : p.args[Number(i.unscaled)];
|
|
654
|
+
}
|
|
455
655
|
/** a numeric's special values, as numeric_out writes them (a text read by numeric_in may be one) */
|
|
456
656
|
const SPECIAL = /^(NaN|-?Infinity)$/;
|
|
457
657
|
/** A special value the program passes through whole (a cell, padded or under a typmod), else undefined; arithmetic on
|
|
458
|
-
* one is refused by name (Postgres's NaN and ±Infinity rules are not this module's).
|
|
459
|
-
|
|
658
|
+
* one is refused by name (Postgres's NaN and ±Infinity rules are not this module's). Once per node for the row
|
|
659
|
+
* (`seen`), as `evaluate` is. */
|
|
660
|
+
function specialOf(p, row, memo, seen) {
|
|
661
|
+
if (seen.has(p))
|
|
662
|
+
return seen.get(p);
|
|
663
|
+
const v = specialNode(p, row, memo, seen);
|
|
664
|
+
seen.set(p, v);
|
|
665
|
+
return v;
|
|
666
|
+
}
|
|
667
|
+
function specialNode(p, row, memo, seen) {
|
|
460
668
|
if (p.op === "cell") {
|
|
461
669
|
const v = row[p.key];
|
|
462
670
|
return typeof v === "string" && SPECIAL.test(v) ? v : undefined;
|
|
463
671
|
}
|
|
464
672
|
if (p.op === "pad" || p.op === "rescale")
|
|
465
|
-
return specialOf(p.arg, row);
|
|
673
|
+
return specialOf(p.arg, row, memo, seen);
|
|
466
674
|
// the arm the row took (a special value in another is never read, as Postgres computes only the taken arm)
|
|
467
675
|
if (p.op === "pick") {
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
return arm === undefined ? undefined : specialOf(arm, row);
|
|
676
|
+
const arm = takenArm(p, (x) => evaluate(x, row, memo));
|
|
677
|
+
return arm === undefined ? undefined : specialOf(arm, row, memo, seen);
|
|
471
678
|
}
|
|
472
679
|
if (p.op === "typmod") {
|
|
473
|
-
const v = specialOf(p.arg, row);
|
|
474
|
-
if (v !== undefined && v !== "NaN")
|
|
475
|
-
|
|
680
|
+
const v = specialOf(p.arg, row, memo, seen);
|
|
681
|
+
if (v !== undefined && v !== "NaN") {
|
|
682
|
+
const o = fieldOverflow(p.precision, p.scale, true);
|
|
683
|
+
throw new NumericError(o.message, "22003", null, o.detail);
|
|
684
|
+
}
|
|
476
685
|
return v;
|
|
477
686
|
}
|
|
478
687
|
for (const key of cellsOf(p)) {
|
|
479
688
|
const v = row[key];
|
|
480
689
|
if (typeof v === "string" && SPECIAL.test(v))
|
|
481
|
-
throw new NumericError(`arithmetic on a numeric
|
|
690
|
+
throw new NumericError(`arithmetic on a numeric NaN or Infinity is not supported by Mason (${gapMark("numeric-special")})`, "0A000", "numeric-special");
|
|
482
691
|
}
|
|
483
692
|
return undefined;
|
|
484
693
|
}
|
|
485
|
-
/** The program's value as `numeric_out` writes it, or null. */
|
|
694
|
+
/** The program's value as `numeric_out` writes it, or null: each node computed at most once for the row. */
|
|
486
695
|
export const evaluateText = (p, row) => {
|
|
487
|
-
const
|
|
696
|
+
const memo = new Map();
|
|
697
|
+
const special = specialOf(p, row, memo, new Map());
|
|
488
698
|
if (special !== undefined)
|
|
489
699
|
return special;
|
|
490
|
-
const v = evaluate(p, row);
|
|
700
|
+
const v = evaluate(p, row, memo);
|
|
491
701
|
return v === null ? null : numericText(v);
|
|
492
702
|
};
|
|
703
|
+
/** A program with each cell replaced (a parameter's `$n` too), its SHARING KEPT: a node several parents share is mapped
|
|
704
|
+
* once and its one image shared alike, and `f` is asked once per cell node. */
|
|
705
|
+
export function mapCells(p, f) {
|
|
706
|
+
const memo = new Map();
|
|
707
|
+
const m = (x) => {
|
|
708
|
+
const known = memo.get(x);
|
|
709
|
+
if (known !== undefined)
|
|
710
|
+
return known;
|
|
711
|
+
const y = mapNode(x, m, f);
|
|
712
|
+
memo.set(x, y);
|
|
713
|
+
return y;
|
|
714
|
+
};
|
|
715
|
+
return m(p);
|
|
716
|
+
}
|
|
717
|
+
function mapNode(p, m, f) {
|
|
718
|
+
switch (p.op) {
|
|
719
|
+
case "cell": return f(p.key);
|
|
720
|
+
case "literal": return p;
|
|
721
|
+
case "add":
|
|
722
|
+
case "sub":
|
|
723
|
+
case "mul":
|
|
724
|
+
case "div":
|
|
725
|
+
case "mod": return { op: p.op, args: [m(p.args[0]), m(p.args[1])] };
|
|
726
|
+
case "neg":
|
|
727
|
+
case "abs":
|
|
728
|
+
case "floor":
|
|
729
|
+
case "ceil": return { op: p.op, arg: m(p.arg) };
|
|
730
|
+
case "round":
|
|
731
|
+
case "trunc":
|
|
732
|
+
case "pad": return { op: p.op, arg: m(p.arg), scale: p.scale };
|
|
733
|
+
case "typmod": return { op: "typmod", arg: m(p.arg), precision: p.precision, scale: p.scale };
|
|
734
|
+
case "greatest":
|
|
735
|
+
case "least":
|
|
736
|
+
case "coalesce": return { op: p.op, args: p.args.map(m) };
|
|
737
|
+
case "stddev": return { op: "stddev", args: [m(p.args[0]), m(p.args[1]), m(p.args[2])], sample: p.sample, variance: p.variance };
|
|
738
|
+
case "pick": return { op: "pick", index: m(p.index), args: p.args.map(m) };
|
|
739
|
+
case "rescale": return { op: "rescale", arg: m(p.arg), scale: m(p.scale) };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* A PROGRAM AS TEXT, THE ONE FORMAT a program leaves the planner in (the literal `__mason_numeric_program` is given, a
|
|
744
|
+
* record compared or measured): a table of its nodes, each DISTINCT node written once with its children as their places
|
|
745
|
+
* in the table, children before parents, and the root's place:
|
|
746
|
+
* `{"n":[{"op":"cell","key":"0"},{"op":"add","args":[0,0]}],"r":1}`. Its size is linear in the program's distinct
|
|
747
|
+
* nodes, however many parents share one. It is also the program's STRUCTURAL IDENTITY: each node is interned by its op,
|
|
748
|
+
* its fields and its children's places, so two programs equal as trees are one text whatever their sharing (a tree and
|
|
749
|
+
* the same tree built with sharing; two arms built apart), and two that differ are two.
|
|
750
|
+
*/
|
|
751
|
+
export function programJson(p) {
|
|
752
|
+
const nodes = [], interned = new Map(), at = new Map();
|
|
753
|
+
const place = (x) => {
|
|
754
|
+
const known = at.get(x);
|
|
755
|
+
if (known !== undefined)
|
|
756
|
+
return known;
|
|
757
|
+
const text = nodeJson(x, place);
|
|
758
|
+
let i = interned.get(text);
|
|
759
|
+
if (i === undefined) {
|
|
760
|
+
i = nodes.length;
|
|
761
|
+
nodes.push(text);
|
|
762
|
+
interned.set(text, i);
|
|
763
|
+
}
|
|
764
|
+
at.set(x, i);
|
|
765
|
+
return i;
|
|
766
|
+
};
|
|
767
|
+
const root = place(p);
|
|
768
|
+
return `{"n":[${nodes.join(",")}],"r":${root}}`;
|
|
769
|
+
}
|
|
770
|
+
/** one node's entry in `programJson`'s table: its fields in one order whatever order they were made in, a child as its place */
|
|
771
|
+
function nodeJson(p, place) {
|
|
772
|
+
const j = JSON.stringify;
|
|
773
|
+
const list = (xs) => xs.map(place).join(",");
|
|
774
|
+
switch (p.op) {
|
|
775
|
+
case "cell": return `{"op":"cell","key":${j(p.key)}}`;
|
|
776
|
+
case "literal": return `{"op":"literal","text":${j(p.text)}}`;
|
|
777
|
+
case "add":
|
|
778
|
+
case "sub":
|
|
779
|
+
case "mul":
|
|
780
|
+
case "div":
|
|
781
|
+
case "mod":
|
|
782
|
+
case "greatest":
|
|
783
|
+
case "least":
|
|
784
|
+
case "coalesce":
|
|
785
|
+
return `{"op":${j(p.op)},"args":[${list(p.args)}]}`;
|
|
786
|
+
case "neg":
|
|
787
|
+
case "abs":
|
|
788
|
+
case "floor":
|
|
789
|
+
case "ceil": return `{"op":${j(p.op)},"arg":${place(p.arg)}}`;
|
|
790
|
+
case "round":
|
|
791
|
+
case "trunc":
|
|
792
|
+
case "pad": return `{"op":${j(p.op)},"arg":${place(p.arg)},"scale":${j(p.scale)}}`;
|
|
793
|
+
case "typmod": return `{"op":"typmod","arg":${place(p.arg)},"precision":${j(p.precision)},"scale":${j(p.scale)}}`;
|
|
794
|
+
case "stddev": return `{"op":"stddev","args":[${list(p.args)}],"sample":${j(p.sample)},"variance":${j(p.variance)}}`;
|
|
795
|
+
case "pick": {
|
|
796
|
+
const index = place(p.index);
|
|
797
|
+
return `{"op":"pick","index":${index},"args":[${list(p.args)}]}`;
|
|
798
|
+
}
|
|
799
|
+
case "rescale": {
|
|
800
|
+
const arg = place(p.arg);
|
|
801
|
+
return `{"op":"rescale","arg":${arg},"scale":${place(p.scale)}}`;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/** A program from `programJson`'s text: each node made once, every parent of it sharing it (a child's place is always
|
|
806
|
+
* before its parent's, so one pass makes them). Throws on a text that is not one. */
|
|
807
|
+
export function parseProgram(text) {
|
|
808
|
+
const raw = JSON.parse(text);
|
|
809
|
+
if (!Array.isArray(raw.n) || typeof raw.r !== "number")
|
|
810
|
+
throw new Error("not a numeric program's text");
|
|
811
|
+
const made = [];
|
|
812
|
+
const child = (i) => {
|
|
813
|
+
if (typeof i !== "number" || !(i >= 0 && i < made.length))
|
|
814
|
+
throw new Error("not a numeric program's text: a child after its parent");
|
|
815
|
+
return made[i];
|
|
816
|
+
};
|
|
817
|
+
for (const n of raw.n) {
|
|
818
|
+
const node = { ...n };
|
|
819
|
+
if (Array.isArray(n.args))
|
|
820
|
+
node.args = n.args.map(child);
|
|
821
|
+
if (n.arg !== undefined)
|
|
822
|
+
node.arg = child(n.arg);
|
|
823
|
+
if (n.index !== undefined)
|
|
824
|
+
node.index = child(n.index);
|
|
825
|
+
if (n.op === "rescale")
|
|
826
|
+
node.scale = child(n.scale);
|
|
827
|
+
made.push(node);
|
|
828
|
+
}
|
|
829
|
+
return child(raw.r);
|
|
830
|
+
}
|