@mason-data/cli 0.8.0 → 0.9.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.
Files changed (109) hide show
  1. package/README.md +13 -4
  2. package/dist/src/api/postgres/catalog-worker.js +49 -2
  3. package/dist/src/api/postgres/catalog.js +166 -32
  4. package/dist/src/api/postgres/listener.js +11 -3
  5. package/dist/src/api/postgres/proxy.js +283 -0
  6. package/dist/src/api/postgres/schema.js +6 -0
  7. package/dist/src/api/postgres/server.js +505 -99
  8. package/dist/src/api/postgres/translate.js +47 -2
  9. package/dist/src/cli/background.js +18 -19
  10. package/dist/src/cli/bridge-stdin.js +101 -0
  11. package/dist/src/cli/command.js +169 -11
  12. package/dist/src/cli/commands.js +207 -51
  13. package/dist/src/cli/credentials.js +182 -0
  14. package/dist/src/cli/demo.js +2 -2
  15. package/dist/src/cli/deploy.js +574 -0
  16. package/dist/src/cli/dev.js +148 -27
  17. package/dist/src/cli/doctor.js +116 -33
  18. package/dist/src/cli/generated-files.js +143 -0
  19. package/dist/src/cli/guides.js +97 -6
  20. package/dist/src/cli/hosted.js +850 -0
  21. package/dist/src/cli/init.js +47 -11
  22. package/dist/src/cli/login.js +289 -0
  23. package/dist/src/cli/machine.js +1 -1
  24. package/dist/src/cli/main.js +8 -2
  25. package/dist/src/cli/mason.cjs +17 -0
  26. package/dist/src/cli/mcp.js +2 -2
  27. package/dist/src/cli/output-gate.js +176 -0
  28. package/dist/src/cli/page-share.js +96 -0
  29. package/dist/src/cli/pg-tls.js +1 -1
  30. package/dist/src/cli/pin.js +6 -2
  31. package/dist/src/cli/pool-size.cjs +85 -0
  32. package/dist/src/cli/ports.js +2 -0
  33. package/dist/src/cli/preflight.js +130 -39
  34. package/dist/src/cli/project.js +99 -0
  35. package/dist/src/cli/readiness.js +42 -6
  36. package/dist/src/cli/registry.js +12 -4
  37. package/dist/src/cli/replica-hosted.js +665 -0
  38. package/dist/src/cli/replica.js +76 -0
  39. package/dist/src/cli/roles.js +2 -1
  40. package/dist/src/cli/run-server.js +154 -7
  41. package/dist/src/cli/security.js +57 -10
  42. package/dist/src/cli/serve.js +63 -7
  43. package/dist/src/cli/share-run.js +51 -8
  44. package/dist/src/cli/share.js +3 -3
  45. package/dist/src/cli/source-edit.js +10 -4
  46. package/dist/src/cli/source-role.js +344 -0
  47. package/dist/src/cli/sources.js +30 -20
  48. package/dist/src/cli/style.js +2 -1
  49. package/dist/src/cli/supervise.js +15 -23
  50. package/dist/src/cli/token-hosted.js +337 -0
  51. package/dist/src/cli/ui-ssh.js +460 -0
  52. package/dist/src/cli/ui.js +115 -11
  53. package/dist/src/cli/user-hosted.js +299 -0
  54. package/dist/src/cli/user.js +126 -74
  55. package/dist/src/demo/seed.js +26 -0
  56. package/dist/src/duck/analyze.js +36 -20
  57. package/dist/src/duck/collation.js +266 -0
  58. package/dist/src/duck/constant-key.js +94 -0
  59. package/dist/src/duck/distinct-key.js +283 -26
  60. package/dist/src/duck/events.js +39 -8
  61. package/dist/src/duck/executor-worker.js +2 -2
  62. package/dist/src/duck/executor.js +6 -5
  63. package/dist/src/duck/explain.js +4 -0
  64. package/dist/src/duck/functions.js +448 -49
  65. package/dist/src/duck/gaps.js +34 -16
  66. package/dist/src/duck/generated-plan.js +1070 -0
  67. package/dist/src/duck/generated.js +906 -0
  68. package/dist/src/duck/grants.js +1 -1
  69. package/dist/src/duck/held-types.js +64 -0
  70. package/dist/src/duck/history.js +4 -0
  71. package/dist/src/duck/jsonb-order.js +9 -12
  72. package/dist/src/duck/kept-route.js +6 -5
  73. package/dist/src/duck/kept.js +9 -7
  74. package/dist/src/duck/late.js +1469 -40
  75. package/dist/src/duck/libc-bound.js +341 -0
  76. package/dist/src/duck/libc-order.js +892 -126
  77. package/dist/src/duck/pg-eval-worker.js +7 -1
  78. package/dist/src/duck/pg-eval.js +6 -4
  79. package/dist/src/duck/pgtree.js +774 -23
  80. package/dist/src/duck/plan.js +275 -30
  81. package/dist/src/duck/publication.js +14 -0
  82. package/dist/src/duck/requests.js +9 -5
  83. package/dist/src/duck/resolved.js +117 -2
  84. package/dist/src/duck/rewrite.js +4567 -583
  85. package/dist/src/duck/row-filter.js +12 -6
  86. package/dist/src/duck/statement.js +19 -0
  87. package/dist/src/duck/store.js +294 -68
  88. package/dist/src/duck/stream.js +9 -5
  89. package/dist/src/duck/sync.js +602 -126
  90. package/dist/src/duck/token.js +4 -1
  91. package/dist/src/duck/types.js +63 -7
  92. package/dist/src/duck/usage.js +6 -4
  93. package/dist/src/duck/users.js +5 -0
  94. package/dist/src/duck/verified.js +159 -2
  95. package/dist/src/duck/views.js +4 -3
  96. package/dist/src/lib/database-url.js +55 -10
  97. package/dist/src/lib/instant.js +50 -0
  98. package/dist/src/lib/log-buffer.js +13 -4
  99. package/dist/src/lib/log-read.js +196 -0
  100. package/dist/src/lib/pg-numeric.js +387 -49
  101. package/dist/src/lib/secret-mask.js +142 -0
  102. package/dist/src/lib/table-sync.js +24 -9
  103. package/dist/src/web/app.js +9 -4
  104. package/dist/src/web/pages/status.js +178 -13
  105. package/dist/src/web/pages/syncs.js +1 -1
  106. package/dist/src/web/pages/tables.js +8 -3
  107. package/dist/src/web/pages/users.js +4 -3
  108. package/dist/src/web/shared.js +19 -3
  109. 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
- constructor(message, sqlstate, gap = null) { super(message); this.sqlstate = sqlstate; this.gap = gap; }
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
- const pow10 = (n) => 10n ** BigInt(n);
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 ga = groups(a), gb = groups(b);
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
- 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");
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
- /** The cells a program reads. */
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": return [p.key];
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": return [...cellsOf(p.args[0]), ...cellsOf(p.args[1])];
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 cellsOf(p.arg);
358
- case "greatest":
359
- case "least":
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
- /** The value of a program over one row, or null; throws NumericError where Postgres raises (a division by zero). */
367
- export function evaluate(p, row) {
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": return parseNumeric(p.text);
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 = evaluate(p.arg, row);
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 = evaluate(p.arg, row);
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 = evaluate(p.arg, row);
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 = evaluate(p.arg, row);
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 = evaluate(p.arg, row);
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 = evaluate(p.arg, row), s = evaluate(p.scale, row);
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 = evaluate(p.arg, row);
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 = evaluate(p.arg, row);
605
+ const a = ev(p.arg);
410
606
  return a === null ? null : floor(a);
411
607
  }
412
608
  case "ceil": {
413
- const a = evaluate(p.arg, row);
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 = evaluate(x, row);
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 = evaluate(x, row);
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 = evaluate(p.args[0], row), b = evaluate(p.args[1], row);
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 i = evaluate(p.index, row);
440
- const arm = i === null || i.scale !== 0 ? undefined : p.args[Number(i.unscaled)];
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((a) => evaluate(a, row));
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 = evaluate(p.args[0], row), b = evaluate(p.args[1], row);
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
- function specialOf(p, row) {
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 i = evaluate(p.index, row);
469
- const arm = i === null || i.scale !== 0 ? undefined : p.args[Number(i.unscaled)];
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
- throw new NumericError("numeric field overflow", "22003");
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 ${v} is not supported by Mason (${gapMark("numeric-special")})`, "0A000", "numeric-special");
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 special = specialOf(p, row);
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
+ }