@mason-data/cli 0.0.0-stage → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +209 -2
  3. package/dist/src/api/error.js +18 -0
  4. package/dist/src/api/postgres/catalog-worker.js +92 -0
  5. package/dist/src/api/postgres/catalog.js +442 -0
  6. package/dist/src/api/postgres/encode.js +541 -0
  7. package/dist/src/api/postgres/explain.js +143 -0
  8. package/dist/src/api/postgres/listener.js +60 -0
  9. package/dist/src/api/postgres/parser-worker.js +58 -0
  10. package/dist/src/api/postgres/parser.js +121 -0
  11. package/dist/src/api/postgres/plain.js +33 -0
  12. package/dist/src/api/postgres/protocol.js +404 -0
  13. package/dist/src/api/postgres/schema.js +54 -0
  14. package/dist/src/api/postgres/server.js +1670 -0
  15. package/dist/src/api/postgres/translate.js +248 -0
  16. package/dist/src/api/request-log.js +50 -0
  17. package/dist/src/cdc/pg-wire.js +482 -0
  18. package/dist/src/cdc/pgoutput.js +107 -0
  19. package/dist/src/cdc/poll.js +67 -0
  20. package/dist/src/cdc/source.js +65 -0
  21. package/dist/src/cli/background.js +174 -0
  22. package/dist/src/cli/command.js +172 -0
  23. package/dist/src/cli/commands.js +1145 -0
  24. package/dist/src/cli/control.js +36 -0
  25. package/dist/src/cli/dev.js +576 -0
  26. package/dist/src/cli/doctor.js +257 -0
  27. package/dist/src/cli/guides.js +134 -0
  28. package/dist/src/cli/init.js +326 -0
  29. package/dist/src/cli/machine.js +157 -0
  30. package/dist/src/cli/main.js +83 -0
  31. package/dist/src/cli/pg-tls.js +100 -0
  32. package/dist/src/cli/pg-url.js +52 -0
  33. package/dist/src/cli/pin.js +36 -0
  34. package/dist/src/cli/ports.js +118 -0
  35. package/dist/src/cli/preflight.js +338 -0
  36. package/dist/src/cli/project.js +127 -0
  37. package/dist/src/cli/readiness.js +76 -0
  38. package/dist/src/cli/registry.js +217 -0
  39. package/dist/src/cli/replication-checks.js +120 -0
  40. package/dist/src/cli/replication-mode.js +148 -0
  41. package/dist/src/cli/run-server.js +388 -0
  42. package/dist/src/cli/runs.js +120 -0
  43. package/dist/src/cli/security.js +86 -0
  44. package/dist/src/cli/self-signed-cert.js +64 -0
  45. package/dist/src/cli/serve.js +296 -0
  46. package/dist/src/cli/sources.js +562 -0
  47. package/dist/src/cli/state.js +58 -0
  48. package/dist/src/cli/style.js +187 -0
  49. package/dist/src/cli/supervise.js +248 -0
  50. package/dist/src/cli/sync.js +94 -0
  51. package/dist/src/cli/ui.js +107 -0
  52. package/dist/src/cli/user.js +229 -0
  53. package/dist/src/cli/version.js +23 -0
  54. package/dist/src/duck/analyze.js +113 -0
  55. package/dist/src/duck/cancel.js +49 -0
  56. package/dist/src/duck/executor-worker.js +263 -0
  57. package/dist/src/duck/executor.js +526 -0
  58. package/dist/src/duck/explain.js +119 -0
  59. package/dist/src/duck/functions.js +764 -0
  60. package/dist/src/duck/gaps.js +89 -0
  61. package/dist/src/duck/jsonb.js +186 -0
  62. package/dist/src/duck/kept-route.js +219 -0
  63. package/dist/src/duck/kept.js +797 -0
  64. package/dist/src/duck/late.js +221 -0
  65. package/dist/src/duck/libc-order.js +190 -0
  66. package/dist/src/duck/pg-eval-worker.js +34 -0
  67. package/dist/src/duck/pg-eval.js +59 -0
  68. package/dist/src/duck/pgtree.js +338 -0
  69. package/dist/src/duck/plan.js +246 -0
  70. package/dist/src/duck/publication.js +207 -0
  71. package/dist/src/duck/requests.js +209 -0
  72. package/dist/src/duck/resolved.js +248 -0
  73. package/dist/src/duck/rewrite.js +4001 -0
  74. package/dist/src/duck/store.js +831 -0
  75. package/dist/src/duck/stream.js +518 -0
  76. package/dist/src/duck/sync.js +2864 -0
  77. package/dist/src/duck/to-char.js +482 -0
  78. package/dist/src/duck/token.js +31 -0
  79. package/dist/src/duck/types.js +427 -0
  80. package/dist/src/duck/users.js +231 -0
  81. package/dist/src/duck/verified.js +995 -0
  82. package/dist/src/duck/views.js +150 -0
  83. package/dist/src/lib/database-url.js +74 -0
  84. package/dist/src/lib/did-you-mean.js +43 -0
  85. package/dist/src/lib/dotenv.js +72 -0
  86. package/dist/src/lib/exit-when-written.js +20 -0
  87. package/dist/src/lib/float-equal.js +69 -0
  88. package/dist/src/lib/http-json.js +22 -0
  89. package/dist/src/lib/log.js +161 -0
  90. package/dist/src/lib/pg-numeric.js +489 -0
  91. package/dist/src/lib/port.js +22 -0
  92. package/dist/src/lib/project-file.js +49 -0
  93. package/dist/src/lib/project-files.js +43 -0
  94. package/dist/src/lib/project-version.js +76 -0
  95. package/dist/src/lib/runnable.js +14 -0
  96. package/dist/src/lib/shell-word.js +4 -0
  97. package/dist/src/lib/timezone.js +236 -0
  98. package/dist/src/lib/zone-names.js +114 -0
  99. package/dist/src/web/app.css +65 -0
  100. package/dist/src/web/app.js +336 -0
  101. package/dist/src/web/index.html +39 -0
  102. package/package.json +22 -4
@@ -0,0 +1,489 @@
1
+ /**
2
+ * POSTGRES'S `numeric`, EXACTLY: the one place the Postgres API decides when a value is a `numeric` (oid 1700) and
3
+ * computes its digits as Postgres would (the Cube replay on #499, finding 4: `avg(bigint)` came back as a JSON
4
+ * number missing its last digits where Postgres sends `21.8594916280008069`; what is said to be supported must return
5
+ * the numbers, formats and results Postgres does). Read by the copy's path (src/duck/) and the
6
+ * wire encoder (src/api/postgres/encode.ts); in the ClickHouse stack this was cut from, also by its HTTP body path's
7
+ * metric types and averages and its unrouted SQL path's aggregates and arithmetic.
8
+ *
9
+ * WHEN A VALUE IS EXACT: its inputs are integers (a count, a sum, an integer column, integer arithmetic — ClickHouse
10
+ * computes those exactly and the API carries a 64-bit one as digits) or numeric literals, combined by `+ - * /`,
11
+ * `round`, a cast to `numeric`, `avg` and `sum`. Then the digits are knowable, and this module computes them with
12
+ * Postgres's own rules, in BigInt arithmetic: a sum and a product keep every digit (the result's scale is the larger,
13
+ * or the sum, of the inputs' scales), a quotient is rounded HALF AWAY FROM ZERO at the scale `select_div_scale`
14
+ * (numeric.c) picks — at least 16 significant digits, never fewer decimals than either input shows — and `round(x, n)`
15
+ * rounds half away from zero to n decimals and shows exactly n. An average is the sum divided by the count, as
16
+ * Postgres's `int8_avg` / `numeric_poly_avg` divide them. A value with a float input — a `double precision` column, a
17
+ * source `numeric` column (Mason reads every one as Float64, CLAUDE.md) — is NOT exact, stays `double precision`, and
18
+ * keeps the divergence docs/POSTGRES-API.md declares.
19
+ *
20
+ * THE TEXT FORM is `numeric_out`'s: every digit of the scale written, trailing zeros kept (`avg(1)` is
21
+ * `1.00000000000000000000`), no exponent, `-` for a negative value and never for zero. THE BINARY FORM is
22
+ * `numeric_send`'s: base-10000 digit groups with a weight, a sign and the display scale, leading and trailing zero
23
+ * groups stripped as `make_result` strips them.
24
+ *
25
+ * Pure and dependency-free: tests/pg-numeric.test.ts holds every operation to PGlite's own numeric over generated values.
26
+ */
27
+ import { Buffer } from "node:buffer";
28
+ /** A value this module cannot compute as Postgres does, or an operation Postgres refuses: with Postgres's SQLSTATE. */
29
+ export class NumericError extends Error {
30
+ sqlstate;
31
+ constructor(message, sqlstate) { super(message); this.sqlstate = sqlstate; }
32
+ }
33
+ /** Postgres's limits on a numeric's scale (numeric.h): a quotient's scale is held between these */
34
+ const NUMERIC_MIN_SIG_DIGITS = 16;
35
+ const NUMERIC_MAX_DISPLAY_SCALE = 1000;
36
+ /** the decimal digits in one base-10000 group (numeric.c's DEC_DIGITS, NBASE = 10^DEC_DIGITS) */
37
+ const DEC_DIGITS = 4;
38
+ const pow10 = (n) => 10n ** BigInt(n);
39
+ const abs = (x) => x < 0n ? -x : x;
40
+ /** `x / d` rounded half away from zero (d > 0): how numeric.c's `round_var` and `div_var(…, round = true)` round */
41
+ function divRound(x, d) {
42
+ const q = abs(x) / d;
43
+ const r = abs(x) % d;
44
+ const rounded = 2n * r >= d ? q + 1n : q;
45
+ return x < 0n ? -rounded : rounded;
46
+ }
47
+ /** digits, an underscore allowed between two of them (Postgres 16: `1_000`) */
48
+ const DIGITS = String.raw `\d+(?:_\d+)*`;
49
+ const NUMERIC_TEXT = new RegExp(String.raw `^\s*([+-])?(${DIGITS})?(?:\.(${DIGITS})?)?(?:[eE]([+-]?${DIGITS}))?\s*$`);
50
+ /** Postgres 16's non-decimal integers: `0x1F`, `0o17`, `0b101`, an underscore before any digit (`0x_1F`) */
51
+ const NON_DECIMAL = /^\s*([+-])?0(?:[xX]((?:_?[0-9a-fA-F])+)|[oO]((?:_?[0-7])+)|[bB]((?:_?[01])+))\s*$/;
52
+ /** A numeric from its text, as `numeric_in` reads it (held to Postgres by tests/pg-numeric.test.ts): the scale is the
53
+ * decimals written, less the exponent (never below 0): `1.50` has scale 2, `1.5e3` is 1500 with scale 0, `1e-3` is
54
+ * 0.001 with scale 3; `1_000` is 1000 and `0x1F` is 31 (scale 0). NaN and Infinity are no PgNumeric: refused here. */
55
+ export function parseNumeric(text) {
56
+ const x = NON_DECIMAL.exec(text);
57
+ if (x !== null) {
58
+ const [prefix, digits] = x[2] !== undefined ? ["0x", x[2]] : x[3] !== undefined ? ["0o", x[3]] : ["0b", x[4]];
59
+ const v = BigInt(prefix + digits.replaceAll("_", ""));
60
+ return { unscaled: x[1] === "-" ? -v : v, scale: 0 };
61
+ }
62
+ const m = NUMERIC_TEXT.exec(text);
63
+ if (m === null || (m[2] === undefined && m[3] === undefined))
64
+ throw new NumericError(`invalid input syntax for type numeric: "${text}"`, "22P02");
65
+ const frac = (m[3] ?? "").replaceAll("_", "");
66
+ const exp = m[4] === undefined ? 0 : Number(m[4].replaceAll("_", ""));
67
+ const digits = BigInt(`${(m[2] ?? "").replaceAll("_", "")}${frac}` || "0");
68
+ const shift = frac.length - exp; // the value is digits × 10^-shift
69
+ const scale = Math.max(0, shift);
70
+ const unscaled = digits * pow10(scale - shift);
71
+ return { unscaled: m[1] === "-" ? -unscaled : unscaled, scale };
72
+ }
73
+ /** A text as Postgres's numeric reads it, written back as numeric_out writes it (`1_000.50` → `1000.50`, `0x1F` → `31`,
74
+ * `1.5e1` → `15`); null for a text it reads as no finite numeric (NaN, Infinity, or no numeric at all). */
75
+ export function numericLiteral(text) {
76
+ try {
77
+ return numericText(parseNumeric(text));
78
+ }
79
+ catch (e) {
80
+ if (e instanceof NumericError)
81
+ return null;
82
+ throw e;
83
+ }
84
+ }
85
+ /** `numeric_out`: the digits with exactly `scale` decimals, no exponent; zero is never negative. */
86
+ export function numericText(n) {
87
+ const digits = abs(n.unscaled).toString().padStart(n.scale + 1, "0");
88
+ const whole = digits.slice(0, digits.length - n.scale);
89
+ const text = n.scale === 0 ? whole : `${whole}.${digits.slice(digits.length - n.scale)}`;
90
+ return n.unscaled < 0n ? `-${text}` : text;
91
+ }
92
+ /** The value at a larger scale (more trailing zeros), unchanged. */
93
+ const atScale = (n, scale) => n.unscaled * pow10(scale - n.scale);
94
+ export const add = (a, b) => {
95
+ const scale = Math.max(a.scale, b.scale);
96
+ return { unscaled: atScale(a, scale) + atScale(b, scale), scale };
97
+ };
98
+ export const sub = (a, b) => add(a, { unscaled: -b.unscaled, scale: b.scale });
99
+ /** `numeric_mul`: exact, at the sum of the scales */
100
+ export const mul = (a, b) => ({ unscaled: a.unscaled * b.unscaled, scale: a.scale + b.scale });
101
+ /** The base-10000 groups of a value, most significant first, and the weight of the first (numeric.c's NumericVar:
102
+ * the group holding the units has weight 0, the four decimals after the point weight -1). Zero groups at either end
103
+ * are dropped, as `make_result` drops them; zero is no group at all, at weight 0. */
104
+ function groups(n) {
105
+ if (n.unscaled === 0n)
106
+ return { digits: [], weight: 0 };
107
+ // align the decimal point on a group boundary: pad the decimals to a multiple of four
108
+ const pad = (DEC_DIGITS - (n.scale % DEC_DIGITS)) % DEC_DIGITS;
109
+ let u = abs(n.unscaled) * pow10(pad);
110
+ const fracGroups = (n.scale + pad) / DEC_DIGITS;
111
+ const lsFirst = [];
112
+ while (u > 0n) {
113
+ lsFirst.push(Number(u % 10000n));
114
+ u /= 10000n;
115
+ }
116
+ let low = 0;
117
+ while (lsFirst[low] === 0)
118
+ low++;
119
+ const digits = lsFirst.slice(low).reverse();
120
+ return { digits, weight: lsFirst.length - 1 - fracGroups };
121
+ }
122
+ /**
123
+ * `select_div_scale` (numeric.c), faithfully: a quotient keeps at least NUMERIC_MIN_SIG_DIGITS significant digits —
124
+ * the scale is read from the WEIGHTS and FIRST GROUPS of the two values, so it depends on the values, not only their
125
+ * types — and never fewer decimals than either input shows, between 0 and NUMERIC_MAX_DISPLAY_SCALE.
126
+ */
127
+ export function divScale(a, b) {
128
+ const ga = groups(a), gb = groups(b);
129
+ const weight1 = ga.digits.length ? ga.weight : 0, first1 = ga.digits[0] ?? 0;
130
+ const weight2 = gb.digits.length ? gb.weight : 0, first2 = gb.digits[0] ?? 0;
131
+ // the quotient's weight; when the first groups are equal it may be either, and the smaller is assumed
132
+ let qweight = weight1 - weight2;
133
+ if (first1 <= first2)
134
+ qweight--;
135
+ let rscale = NUMERIC_MIN_SIG_DIGITS - qweight * DEC_DIGITS;
136
+ rscale = Math.max(rscale, a.scale, b.scale, 0);
137
+ return Math.min(rscale, NUMERIC_MAX_DISPLAY_SCALE);
138
+ }
139
+ /** `numeric_div`: rounded half away from zero at `divScale`; a zero divisor is Postgres's 22012. */
140
+ export function div(a, b) {
141
+ if (b.unscaled === 0n)
142
+ throw new NumericError("division by zero", "22012");
143
+ const scale = divScale(a, b);
144
+ // a / b × 10^scale = a.u × 10^(scale - a.s + b.s) / b.u
145
+ const e = scale - a.scale + b.scale;
146
+ let num = a.unscaled, den = b.unscaled;
147
+ if (e >= 0)
148
+ num *= pow10(e);
149
+ else
150
+ den *= pow10(-e);
151
+ if (den < 0n) {
152
+ num = -num;
153
+ den = -den;
154
+ }
155
+ return { unscaled: divRound(num, den), scale };
156
+ }
157
+ /** `round(numeric, n)`: half away from zero to n decimals, shown with exactly n (none when n < 0, which rounds to tens,
158
+ * hundreds …). Postgres clamps n to ±NUMERIC_MAX_RESULT_SCALE (2000). */
159
+ export function round(a, n) {
160
+ const s = Math.max(-2000, Math.min(2000, Math.trunc(n)));
161
+ if (s >= a.scale)
162
+ return { unscaled: atScale(a, s), scale: s };
163
+ const unscaled = divRound(a.unscaled, pow10(a.scale - s));
164
+ return s >= 0 ? { unscaled, scale: s } : { unscaled: unscaled * pow10(-s), scale: 0 };
165
+ }
166
+ /** `trunc(numeric, n)`: toward zero to n decimals, shown with exactly n (none when n < 0) */
167
+ export function trunc(a, n) {
168
+ const s = Math.max(-2000, Math.min(2000, Math.trunc(n)));
169
+ if (s >= a.scale)
170
+ return { unscaled: atScale(a, s), scale: s };
171
+ const unscaled = a.unscaled / pow10(a.scale - s); // BigInt division truncates toward zero
172
+ return s >= 0 ? { unscaled, scale: s } : { unscaled: unscaled * pow10(-s), scale: 0 };
173
+ }
174
+ /** the integer square root of x ≥ 0, rounded down (Newton's method) */
175
+ function isqrt(x) {
176
+ if (x < 2n)
177
+ return x;
178
+ let y = 1n << BigInt((x.toString(2).length >> 1) + 1);
179
+ for (;;) {
180
+ const z = (y + x / y) >> 1n;
181
+ if (z >= y)
182
+ return y;
183
+ y = z;
184
+ }
185
+ }
186
+ /** `sqrt_var` of a non-negative numeric at `rscale` decimals: the exact root rounded half up there (numeric.c computes
187
+ * it truncated past rscale, then round_var rounds) */
188
+ export function sqrt(a, rscale) {
189
+ // √(u / 10^s) × 10^(r+1) = √(u × 10^(2r + 2 - s))
190
+ const e = 2 * rscale + 2 - a.scale;
191
+ const tenths = e >= 0 ? isqrt(a.unscaled * pow10(e)) : isqrt(a.unscaled / pow10(-e));
192
+ return { unscaled: (tenths + 5n) / 10n, scale: rscale };
193
+ }
194
+ /** `numeric_stddev_internal`: stddev or variance (sample or population) from N, Σx and Σx², each exact, as Postgres
195
+ * computes them — the numerator N·Σx² − (Σx)² at twice Σx's scale, zero when not positive; the quotient by N(N−1) or N²
196
+ * at the division's scale; its square root at that scale. Null for no row, or one row of a sample. */
197
+ export function stddev(n, sumX, sumX2, sample, variance) {
198
+ const count = n.unscaled / pow10(n.scale);
199
+ if (count === 0n || (sample && count <= 1n))
200
+ return null;
201
+ const rscale = sumX.scale * 2;
202
+ const numerator = sub(round(mul(n, sumX2), rscale), round(mul(sumX, sumX), rscale));
203
+ if (numerator.unscaled <= 0n)
204
+ return { unscaled: 0n, scale: 0 };
205
+ const denominator = { unscaled: sample ? count * (count - 1n) : count * count, scale: 0 };
206
+ const quotient = div(numerator, denominator);
207
+ return variance ? quotient : sqrt(quotient, quotient.scale);
208
+ }
209
+ /** `numeric_mod`: the remainder of the quotient truncated to an integer, with the sign of the dividend, at the larger
210
+ * scale; a zero divisor is 22012 */
211
+ export function mod(a, b) {
212
+ if (b.unscaled === 0n)
213
+ throw new NumericError("division by zero", "22012");
214
+ const scale = Math.max(a.scale, b.scale);
215
+ return { unscaled: atScale(a, scale) % atScale(b, scale), scale };
216
+ }
217
+ /** `floor` / `ceil` of a numeric: an integer, scale 0 */
218
+ export function floor(a) {
219
+ const d = pow10(a.scale);
220
+ const q = a.unscaled / d;
221
+ return { unscaled: a.unscaled < 0n && q * d !== a.unscaled ? q - 1n : q, scale: 0 };
222
+ }
223
+ export function ceil(a) {
224
+ const d = pow10(a.scale);
225
+ const q = a.unscaled / d;
226
+ return { unscaled: a.unscaled > 0n && q * d !== a.unscaled ? q + 1n : q, scale: 0 };
227
+ }
228
+ /** the order of two numerics by value (`1.50` and `1.5` are equal) */
229
+ export function compare(a, b) {
230
+ const scale = Math.max(a.scale, b.scale);
231
+ const x = atScale(a, scale), y = atScale(b, scale);
232
+ return x < y ? -1 : x > y ? 1 : 0;
233
+ }
234
+ /** A cast to `numeric(p, s)`: rounded as `round` rounds, refused as Postgres refuses (22003) a value of absolute value
235
+ * 10^(p - s) or more — `apply_typmod`'s count of the digits from the first significant one to the point, which is
236
+ * negative below 1, so a scale above the precision (Postgres 15+) is held too: `0.5::numeric(2, 3)` overflows. */
237
+ export function withTypmod(a, precision, scale) {
238
+ const r = round(a, scale);
239
+ const maxDigits = precision - scale;
240
+ const digits = r.unscaled === 0n ? null : abs(r.unscaled).toString().length - r.scale;
241
+ if (digits !== null && digits > maxDigits) {
242
+ throw new NumericError(`numeric field overflow: a field with precision ${precision}, scale ${scale} must round to an absolute value less than ${maxDigits === 0 ? "1" : `10^${maxDigits}`}`, "22003");
243
+ }
244
+ return r;
245
+ }
246
+ /** `numeric_send`: ndigits, weight, sign (0x0000 positive, 0x4000 negative), dscale, then each base-10000 group, all
247
+ * 16-bit big-endian. */
248
+ export function numericBinary(n) {
249
+ const g = groups(n);
250
+ const b = Buffer.alloc(8 + 2 * g.digits.length);
251
+ b.writeInt16BE(g.digits.length, 0);
252
+ b.writeInt16BE(g.weight, 2);
253
+ b.writeUInt16BE(n.unscaled < 0n ? 0x4000 : 0x0000, 4);
254
+ b.writeInt16BE(n.scale, 6);
255
+ g.digits.forEach((d, i) => b.writeInt16BE(d, 8 + 2 * i));
256
+ return b;
257
+ }
258
+ /** A binary-format `numeric` parameter (`numeric_recv`'s layout, the inverse of `numericBinary`), as its text. */
259
+ export function numericFromBinary(bytes) {
260
+ if (bytes.length < 8)
261
+ throw new NumericError(`incorrect binary data format: ${bytes.length} byte(s) for numeric`, "22P03");
262
+ const ndigits = bytes.readInt16BE(0), weight = bytes.readInt16BE(2), sign = bytes.readUInt16BE(4), scale = bytes.readInt16BE(6);
263
+ if (bytes.length !== 8 + 2 * ndigits || ndigits < 0 || scale < 0 || (sign !== 0 && sign !== 0x4000)) {
264
+ throw new NumericError(`incorrect binary data format: ${bytes.length} byte(s), ${ndigits} digit group(s), sign 0x${sign.toString(16)} for numeric`, "22P03");
265
+ }
266
+ // the groups as one integer, then placed by the weight: the value is that integer × 10000^(weight - ndigits + 1)
267
+ let u = 0n;
268
+ for (let i = 0; i < ndigits; i++) {
269
+ const d = bytes.readInt16BE(8 + 2 * i);
270
+ if (d < 0 || d > 9999)
271
+ throw new NumericError(`invalid digit group ${d} in a binary numeric`, "22P03");
272
+ u = u * 10000n + BigInt(d);
273
+ }
274
+ const e = DEC_DIGITS * (weight - ndigits + 1); // u × 10^e
275
+ const unscaled = e + scale >= 0 ? u * pow10(e + scale) : divRound(u, pow10(-(e + scale)));
276
+ return numericText({ unscaled: sign === 0x4000 ? -unscaled : unscaled, scale });
277
+ }
278
+ /** A numeric text that is a WHOLE NUMBER (`42`, `42.00`, `1e3`), as that integer; null for a fraction or a text that is
279
+ * no numeric. How a numeric parameter keeps its digits on both paths (a whole number is bound as one). */
280
+ export function wholeNumberOf(text) {
281
+ let n;
282
+ try {
283
+ n = parseNumeric(text);
284
+ }
285
+ catch (e) {
286
+ if (e instanceof NumericError)
287
+ return null;
288
+ throw e;
289
+ }
290
+ const unit = pow10(n.scale);
291
+ return n.unscaled % unit === 0n ? n.unscaled / unit : null;
292
+ }
293
+ /** A value the answer carries as a numeric: a decimal text (a 64-bit integer as digits, a numeric's own text) or a safe
294
+ * integer; a fractional JavaScript number has no scale, so it is refused rather than guessed. */
295
+ export function numericOf(value) {
296
+ if (typeof value === "string")
297
+ return parseNumeric(value);
298
+ if (typeof value === "number" && Number.isSafeInteger(value))
299
+ return { unscaled: BigInt(value), scale: 0 };
300
+ throw new NumericError(`cannot read ${JSON.stringify(value)} as an exact numeric: not a decimal text or an integer the number holds exactly`, "XX000");
301
+ }
302
+ const INTEGER = new Set(["int2", "int4", "int8"]);
303
+ const FLOAT = new Set(["float4", "float8"]);
304
+ /**
305
+ * THE TYPE OF AN AGGREGATE, by its argument — Postgres's own table (pg_aggregate): `count` is a bigint; `sum` of a
306
+ * smallint or an integer is a bigint, of a bigint a NUMERIC, of a real a real, of a double precision a double
307
+ * precision; `avg` of any integer is a NUMERIC and of a float a double precision; a percentile is a double precision;
308
+ * `min` / `max` the argument's own type. Null where Postgres has no such aggregate (a sum of a text). A float argument
309
+ * here includes a source `numeric` column, which this API sends as double precision (the declared divergence): its
310
+ * sum and average follow the column's type on this API.
311
+ */
312
+ export function aggregateType(agg, arg) {
313
+ switch (agg) {
314
+ case "count":
315
+ case "count_distinct": return "int8";
316
+ case "sum":
317
+ if (arg === null)
318
+ return null;
319
+ if (arg === "int2" || arg === "int4")
320
+ return "int8";
321
+ if (arg === "int8" || arg === "numeric")
322
+ return "numeric";
323
+ return FLOAT.has(arg) ? arg : null;
324
+ case "average":
325
+ // an argument of no known type (a metric's `sql:` expression) may hold a float: a float8, whose digits nobody
326
+ // claims, never a numeric the answer may not have the digits of
327
+ if (arg === null)
328
+ return "float8";
329
+ if (INTEGER.has(arg) || arg === "numeric")
330
+ return "numeric";
331
+ return FLOAT.has(arg) ? "float8" : null;
332
+ case "percentile": return arg === null || INTEGER.has(arg) || FLOAT.has(arg) || arg === "numeric" ? "float8" : null;
333
+ case "min":
334
+ case "max": return arg;
335
+ }
336
+ }
337
+ /** The cells a program reads. */
338
+ export function cellsOf(p) {
339
+ switch (p.op) {
340
+ case "cell": return [p.key];
341
+ case "literal": return [];
342
+ case "add":
343
+ case "sub":
344
+ case "mul":
345
+ case "div":
346
+ case "mod": return [...cellsOf(p.args[0]), ...cellsOf(p.args[1])];
347
+ case "neg":
348
+ case "round":
349
+ case "typmod":
350
+ case "trunc":
351
+ case "abs":
352
+ case "floor":
353
+ case "ceil":
354
+ case "pad": return cellsOf(p.arg);
355
+ case "greatest":
356
+ case "least":
357
+ case "coalesce":
358
+ case "stddev": return p.args.flatMap(cellsOf);
359
+ case "pick": return [...cellsOf(p.index), ...p.args.flatMap(cellsOf)];
360
+ case "rescale": return [...cellsOf(p.arg), ...cellsOf(p.scale)];
361
+ }
362
+ }
363
+ /** The value of a program over one row, or null; throws NumericError where Postgres raises (a division by zero). */
364
+ export function evaluate(p, row) {
365
+ switch (p.op) {
366
+ case "cell": {
367
+ const v = row[p.key];
368
+ return v === null || v === undefined ? null : numericOf(v);
369
+ }
370
+ case "literal": return parseNumeric(p.text);
371
+ case "neg": {
372
+ const a = evaluate(p.arg, row);
373
+ return a === null ? null : { unscaled: -a.unscaled, scale: a.scale };
374
+ }
375
+ case "round": {
376
+ const a = evaluate(p.arg, row);
377
+ return a === null ? null : round(a, p.scale);
378
+ }
379
+ case "typmod": {
380
+ const a = evaluate(p.arg, row);
381
+ return a === null ? null : withTypmod(a, p.precision, p.scale);
382
+ }
383
+ case "trunc": {
384
+ const a = evaluate(p.arg, row);
385
+ return a === null ? null : trunc(a, p.scale);
386
+ }
387
+ case "pad": {
388
+ const a = evaluate(p.arg, row);
389
+ return a === null || a.scale >= p.scale ? a : { unscaled: a.unscaled * pow10(p.scale - a.scale), scale: p.scale };
390
+ }
391
+ case "rescale": {
392
+ const a = evaluate(p.arg, row), s = evaluate(p.scale, row);
393
+ if (a === null || s === null || s.scale !== 0 || s.unscaled < 0n)
394
+ return a;
395
+ const to = Number(s.unscaled);
396
+ if (to >= a.scale)
397
+ return { unscaled: a.unscaled * pow10(to - a.scale), scale: to };
398
+ const by = pow10(a.scale - to);
399
+ return a.unscaled % by === 0n ? { unscaled: a.unscaled / by, scale: to } : a;
400
+ }
401
+ case "abs": {
402
+ const a = evaluate(p.arg, row);
403
+ return a === null ? null : { unscaled: abs(a.unscaled), scale: a.scale };
404
+ }
405
+ case "floor": {
406
+ const a = evaluate(p.arg, row);
407
+ return a === null ? null : floor(a);
408
+ }
409
+ case "ceil": {
410
+ const a = evaluate(p.arg, row);
411
+ return a === null ? null : ceil(a);
412
+ }
413
+ case "coalesce": {
414
+ for (const x of p.args) {
415
+ const v = evaluate(x, row);
416
+ if (v !== null)
417
+ return v;
418
+ }
419
+ return null;
420
+ }
421
+ case "greatest":
422
+ case "least": {
423
+ let best = null;
424
+ for (const x of p.args) {
425
+ const v = evaluate(x, row);
426
+ if (v !== null && (best === null || (p.op === "greatest" ? compare(v, best) > 0 : compare(v, best) < 0)))
427
+ best = v;
428
+ }
429
+ return best;
430
+ }
431
+ case "mod": {
432
+ const a = evaluate(p.args[0], row), b = evaluate(p.args[1], row);
433
+ return a === null || b === null ? null : mod(a, b);
434
+ }
435
+ case "pick": {
436
+ const i = evaluate(p.index, row);
437
+ const arm = i === null || i.scale !== 0 ? undefined : p.args[Number(i.unscaled)];
438
+ return arm === undefined ? null : evaluate(arm, row);
439
+ }
440
+ case "stddev": {
441
+ const [n, x, x2] = p.args.map((a) => evaluate(a, row));
442
+ return n === null || n === undefined || x === null || x === undefined || x2 === null || x2 === undefined ? null : stddev(n, x, x2, p.sample, p.variance);
443
+ }
444
+ default: {
445
+ const a = evaluate(p.args[0], row), b = evaluate(p.args[1], row);
446
+ if (a === null || b === null)
447
+ return null;
448
+ return p.op === "add" ? add(a, b) : p.op === "sub" ? sub(a, b) : p.op === "mul" ? mul(a, b) : div(a, b);
449
+ }
450
+ }
451
+ }
452
+ /** a numeric's special values, as numeric_out writes them (a text read by numeric_in may be one) */
453
+ const SPECIAL = /^(NaN|-?Infinity)$/;
454
+ /** A special value the program passes through whole (a cell, padded or under a typmod), else undefined; arithmetic on
455
+ * one is refused by name (Postgres's NaN and ±Infinity rules are not this module's). */
456
+ function specialOf(p, row) {
457
+ if (p.op === "cell") {
458
+ const v = row[p.key];
459
+ return typeof v === "string" && SPECIAL.test(v) ? v : undefined;
460
+ }
461
+ if (p.op === "pad" || p.op === "rescale")
462
+ return specialOf(p.arg, row);
463
+ // the arm the row took (a special value in another is never read, as Postgres computes only the taken arm)
464
+ if (p.op === "pick") {
465
+ const i = evaluate(p.index, row);
466
+ const arm = i === null || i.scale !== 0 ? undefined : p.args[Number(i.unscaled)];
467
+ return arm === undefined ? undefined : specialOf(arm, row);
468
+ }
469
+ if (p.op === "typmod") {
470
+ const v = specialOf(p.arg, row);
471
+ if (v !== undefined && v !== "NaN")
472
+ throw new NumericError("numeric field overflow", "22003");
473
+ return v;
474
+ }
475
+ for (const key of cellsOf(p)) {
476
+ const v = row[key];
477
+ if (typeof v === "string" && SPECIAL.test(v))
478
+ throw new NumericError(`arithmetic on a numeric ${v} is not supported by Mason (Mason gap numeric-special)`, "0A000");
479
+ }
480
+ return undefined;
481
+ }
482
+ /** The program's value as `numeric_out` writes it, or null. */
483
+ export const evaluateText = (p, row) => {
484
+ const special = specialOf(p, row);
485
+ if (special !== undefined)
486
+ return special;
487
+ const v = evaluate(p, row);
488
+ return v === null ? null : numericText(v);
489
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A port setting (MASON_API_PORT, MASON_HEALTH_PORT, MASON_WEB_UI_PORT, PG_PORT) is a whole number from 0 to 65535,
3
+ * or the process refuses to start naming the variable and its value — never `parseInt`'s NaN reaching `listen()`.
4
+ * PG_PORT matters most: set, it wins over DATABASE_URL's port (src/lib/database-url.ts), and a Service named `pg`
5
+ * would set it.
6
+ *
7
+ * Kubernetes injects `<SERVICE>_PORT=tcp://<ip>:<port>` into every pod for each Service in its namespace, so a
8
+ * Service named `mason-api` sets MASON_API_PORT (0.1.4 on EKS crash-looped on it). That value is
9
+ * named for what it is, with the fix: `enableServiceLinks: false` on the pod (deploy/k8s/).
10
+ */
11
+ /** The port `raw` names, or the problem with it; `undefined` or empty is the default. */
12
+ export function parsePort(name, raw, fallback) {
13
+ if (raw === undefined || raw === "")
14
+ return { port: fallback };
15
+ if (/^\d{1,5}$/.test(raw) && Number(raw) <= 65535)
16
+ return { port: Number(raw) };
17
+ const serviceLink = /^tcp:\/\//.test(raw)
18
+ ? " — that is a Kubernetes service link (a Service in the namespace is named after the variable): " +
19
+ "set `enableServiceLinks: false` on the pod"
20
+ : "";
21
+ return { problem: `${name}=${raw} is not a port (a whole number from 0 to 65535)${serviceLink}` };
22
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A PROJECT'S `mason.yml` (issue #435): beside its `sources/`, what holds for the whole project — today one key:
3
+ *
4
+ * version: 0.6.0 the Mason version it is written for (src/lib/project-version.ts)
5
+ *
6
+ * Read here, once, by everything that needs it — the image's pin check and the CLI's — so the two cannot read it
7
+ * differently. A key it does not know is refused by name, never ignored. (`timezones:`, `calendar:` and `scope:` — the
8
+ * zones a screen offered, its weeks and named periods, a token's tenant through a join — went with the files an older
9
+ * Mason wrote per table: the run reads none of them.)
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { parse } from "yaml";
14
+ import { didYouMean } from "./did-you-mean.js";
15
+ export const PROJECT_FILE = "mason.yml";
16
+ /** `0.5.1` or `v0.5.1` (the image's MASON_VERSION); null for anything else (`dev`, `main-<sha>`). */
17
+ export function parseVersion(text) {
18
+ const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(text.trim());
19
+ return m ? { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) } : null;
20
+ }
21
+ const isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
22
+ /** The project's settings, or the problem with its mason.yml (a project with none has none of them). */
23
+ export function readProjectSettings(project) {
24
+ const file = path.join(project, PROJECT_FILE);
25
+ if (!fs.existsSync(file))
26
+ return { pin: null, file };
27
+ const fail = (problem) => ({ problem: `${file}: ${problem}`, file });
28
+ let doc;
29
+ try {
30
+ doc = parse(fs.readFileSync(file, "utf8"));
31
+ }
32
+ catch (e) {
33
+ return fail(`is not YAML: ${e.message.split("\n")[0]}`);
34
+ }
35
+ const noVersion = { problem: `${file} names no version: write version: <x.y.z>, the Mason version the project is written for`, file };
36
+ if (doc === null || doc === undefined)
37
+ return noVersion;
38
+ if (!isPlain(doc))
39
+ return fail(`write version: <x.y.z>, the Mason version the project is written for`);
40
+ const stray = Object.keys(doc).filter((k) => k !== "version");
41
+ if (stray.length)
42
+ return fail(`unexpected key ${stray[0]}${didYouMean(stray[0], ["version"])} — mason.yml takes version alone (the tables are the sources file's, sources/<name>.yml)`);
43
+ if (doc.version === undefined || doc.version === null)
44
+ return noVersion;
45
+ const pin = parseVersion(String(doc.version));
46
+ if (pin === null)
47
+ return fail(`version ${JSON.stringify(String(doc.version))} is not a version (x.y.z, e.g. 0.5.1)`);
48
+ return { pin, file };
49
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A PROJECT'S OWN FILES: the YAML under a project folder (its sources file, mason.yml) — what the run fingerprints into
3
+ * its state (src/cli/run-server.ts `runState`), which `mason status` and `mason wait` read (src/cli/commands.ts). In
4
+ * the ClickHouse stack this was cut from, its `mason` service watched them and re-applied on a change in dev.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { createHash } from "node:crypto";
9
+ /** Files whose change means a re-apply: the project's YAML (its sources file, mason.yml). */
10
+ export function isProjectFile(rel) {
11
+ if (rel.split(path.sep).some((p) => p.startsWith(".")))
12
+ return false;
13
+ return /\.ya?ml$/.test(rel);
14
+ }
15
+ /** Every project file (`isProjectFile`) under `project`, as full paths, sorted. */
16
+ export function projectFiles(project) {
17
+ const files = [];
18
+ const walk = (d) => {
19
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
20
+ const p = path.join(d, e.name);
21
+ if (e.isDirectory()) {
22
+ if (!e.name.startsWith(".") && e.name !== "node_modules")
23
+ walk(p);
24
+ }
25
+ else if (isProjectFile(path.relative(project, p)))
26
+ files.push(p);
27
+ }
28
+ };
29
+ walk(project);
30
+ return files.sort();
31
+ }
32
+ /** The project's own files (`isProjectFile`), hashed with their paths. */
33
+ export function hashProjectFiles(project) {
34
+ const h = createHash("sha256");
35
+ for (const f of projectFiles(project))
36
+ h.update(`== ${path.relative(project, f)}\n`).update(fs.readFileSync(f));
37
+ return h;
38
+ }
39
+ /**
40
+ * The project's files as one hash: the run's state's `project` (`RunState`, src/cli/commands.ts) — an edit, an added
41
+ * file and a deleted one each change it; an editor's swap file does not.
42
+ */
43
+ export function projectFingerprint(project) { return hashProjectFiles(project).digest("hex"); }