@mason-data/cli 0.7.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 (155) hide show
  1. package/README.md +101 -73
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +368 -25
  5. package/dist/src/api/postgres/encode.js +3 -1
  6. package/dist/src/api/postgres/explain.js +3 -2
  7. package/dist/src/api/postgres/listener.js +11 -3
  8. package/dist/src/api/postgres/protocol.js +39 -23
  9. package/dist/src/api/postgres/proxy.js +283 -0
  10. package/dist/src/api/postgres/schema.js +6 -0
  11. package/dist/src/api/postgres/server.js +1617 -245
  12. package/dist/src/api/postgres/translate.js +127 -10
  13. package/dist/src/api/request-log.js +18 -5
  14. package/dist/src/cdc/poll.js +5 -5
  15. package/dist/src/cli/background.js +47 -25
  16. package/dist/src/cli/bridge-stdin.js +101 -0
  17. package/dist/src/cli/cancel.js +75 -0
  18. package/dist/src/cli/command.js +170 -11
  19. package/dist/src/cli/commands.js +804 -209
  20. package/dist/src/cli/control.js +59 -0
  21. package/dist/src/cli/credentials.js +182 -0
  22. package/dist/src/cli/demo.js +20 -11
  23. package/dist/src/cli/deploy.js +574 -0
  24. package/dist/src/cli/dev.js +491 -130
  25. package/dist/src/cli/doctor.js +131 -38
  26. package/dist/src/cli/generated-files.js +143 -0
  27. package/dist/src/cli/guides.js +175 -36
  28. package/dist/src/cli/hosted.js +850 -0
  29. package/dist/src/cli/init.js +162 -65
  30. package/dist/src/cli/login.js +289 -0
  31. package/dist/src/cli/machine.js +13 -12
  32. package/dist/src/cli/main.js +11 -5
  33. package/dist/src/cli/mason.cjs +17 -0
  34. package/dist/src/cli/mcp.js +31 -22
  35. package/dist/src/cli/metrics.js +79 -0
  36. package/dist/src/cli/output-gate.js +176 -0
  37. package/dist/src/cli/page-share.js +96 -0
  38. package/dist/src/cli/pg-tls.js +14 -4
  39. package/dist/src/cli/pg-url.js +4 -0
  40. package/dist/src/cli/pin.js +19 -14
  41. package/dist/src/cli/pool-size.cjs +85 -0
  42. package/dist/src/cli/ports.js +2 -0
  43. package/dist/src/cli/preflight.js +138 -46
  44. package/dist/src/cli/project.js +142 -4
  45. package/dist/src/cli/readiness.js +76 -6
  46. package/dist/src/cli/registry.js +65 -38
  47. package/dist/src/cli/replica-hosted.js +665 -0
  48. package/dist/src/cli/replica.js +76 -0
  49. package/dist/src/cli/replication-checks.js +7 -5
  50. package/dist/src/cli/replication-mode.js +105 -26
  51. package/dist/src/cli/request-filter.js +134 -0
  52. package/dist/src/cli/roles.js +585 -0
  53. package/dist/src/cli/run-server.js +637 -48
  54. package/dist/src/cli/runs.js +1 -1
  55. package/dist/src/cli/security.js +67 -18
  56. package/dist/src/cli/serve.js +204 -46
  57. package/dist/src/cli/share-run.js +329 -0
  58. package/dist/src/cli/share.js +286 -0
  59. package/dist/src/cli/source-edit.js +75 -17
  60. package/dist/src/cli/source-role.js +344 -0
  61. package/dist/src/cli/sources.js +306 -47
  62. package/dist/src/cli/style.js +2 -1
  63. package/dist/src/cli/supervise.js +27 -24
  64. package/dist/src/cli/sync.js +55 -25
  65. package/dist/src/cli/token-hosted.js +337 -0
  66. package/dist/src/cli/tunnel.js +200 -0
  67. package/dist/src/cli/ui-ssh.js +460 -0
  68. package/dist/src/cli/ui.js +124 -17
  69. package/dist/src/cli/usage.js +128 -0
  70. package/dist/src/cli/user-hosted.js +299 -0
  71. package/dist/src/cli/user.js +573 -65
  72. package/dist/src/cli/version.js +9 -7
  73. package/dist/src/demo/seed.js +29 -0
  74. package/dist/src/duck/analyze.js +36 -20
  75. package/dist/src/duck/cancel.js +10 -1
  76. package/dist/src/duck/collation.js +266 -0
  77. package/dist/src/duck/constant-key.js +94 -0
  78. package/dist/src/duck/distinct-key.js +283 -26
  79. package/dist/src/duck/events.js +165 -0
  80. package/dist/src/duck/executor-worker.js +20 -11
  81. package/dist/src/duck/executor.js +47 -33
  82. package/dist/src/duck/explain.js +60 -46
  83. package/dist/src/duck/extensions.js +160 -0
  84. package/dist/src/duck/functions.js +461 -63
  85. package/dist/src/duck/gaps.js +48 -21
  86. package/dist/src/duck/generated-plan.js +1070 -0
  87. package/dist/src/duck/generated.js +906 -0
  88. package/dist/src/duck/grants.js +176 -0
  89. package/dist/src/duck/held-types.js +64 -0
  90. package/dist/src/duck/history.js +127 -0
  91. package/dist/src/duck/jsonb-order.js +9 -12
  92. package/dist/src/duck/kept-route.js +6 -5
  93. package/dist/src/duck/kept.js +15 -13
  94. package/dist/src/duck/late.js +1469 -40
  95. package/dist/src/duck/libc-bound.js +341 -0
  96. package/dist/src/duck/libc-order.js +892 -126
  97. package/dist/src/duck/pg-eval-worker.js +7 -1
  98. package/dist/src/duck/pg-eval.js +6 -4
  99. package/dist/src/duck/pgtree.js +774 -23
  100. package/dist/src/duck/plan.js +369 -47
  101. package/dist/src/duck/publication.js +20 -6
  102. package/dist/src/duck/requests.js +269 -93
  103. package/dist/src/duck/resolved.js +117 -2
  104. package/dist/src/duck/rewrite.js +4584 -580
  105. package/dist/src/duck/row-filter.js +655 -0
  106. package/dist/src/duck/statement.js +19 -0
  107. package/dist/src/duck/store.js +439 -177
  108. package/dist/src/duck/stream.js +19 -10
  109. package/dist/src/duck/sync.js +1411 -368
  110. package/dist/src/duck/token.js +4 -1
  111. package/dist/src/duck/types.js +69 -12
  112. package/dist/src/duck/usage.js +326 -0
  113. package/dist/src/duck/users.js +69 -15
  114. package/dist/src/duck/verified.js +161 -1
  115. package/dist/src/duck/views.js +5 -5
  116. package/dist/src/lib/database-name.js +62 -0
  117. package/dist/src/lib/database-url.js +55 -10
  118. package/dist/src/lib/dotenv.js +17 -4
  119. package/dist/src/lib/instant.js +50 -0
  120. package/dist/src/lib/log-buffer.js +77 -0
  121. package/dist/src/lib/log-read.js +196 -0
  122. package/dist/src/lib/pg-numeric.js +390 -49
  123. package/dist/src/lib/project-version.js +23 -23
  124. package/dist/src/lib/secret-mask.js +142 -0
  125. package/dist/src/lib/table-sync.js +282 -0
  126. package/dist/src/web/app.css +706 -66
  127. package/dist/src/web/app.js +331 -409
  128. package/dist/src/web/charts.js +229 -0
  129. package/dist/src/web/index.html +82 -18
  130. package/dist/src/web/pages/queries.js +192 -0
  131. package/dist/src/web/pages/status.js +611 -0
  132. package/dist/src/web/pages/syncs.js +155 -0
  133. package/dist/src/web/pages/tables.js +439 -0
  134. package/dist/src/web/pages/usage.js +397 -0
  135. package/dist/src/web/pages/users.js +219 -0
  136. package/dist/src/web/shared.js +584 -0
  137. package/dist/src/web/vendor/README.md +25 -0
  138. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  139. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  140. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  141. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  142. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  143. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  144. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  145. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  146. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  147. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  148. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  149. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  150. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  151. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  152. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  153. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  154. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  155. package/package.json +12 -4
@@ -25,17 +25,35 @@
25
25
  * Pure and dependency-free: tests/pg-numeric.test.ts holds every operation to PGlite's own numeric over generated values.
26
26
  */
27
27
  import { Buffer } from "node:buffer";
28
+ import { gapMark } from "../duck/gaps.js";
28
29
  /** A value this module cannot compute as Postgres does, or an operation Postgres refuses: with Postgres's SQLSTATE. */
29
30
  export class NumericError extends Error {
30
31
  sqlstate;
31
- constructor(message, sqlstate) { super(message); this.sqlstate = sqlstate; }
32
+ /** the registry entry that refused it (src/duck/gaps.ts): the request log's `gap`; null for Postgres's own error */
33
+ 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}`}.` };
32
47
  }
33
48
  /** Postgres's limits on a numeric's scale (numeric.h): a quotient's scale is held between these */
34
49
  const NUMERIC_MIN_SIG_DIGITS = 16;
35
50
  const NUMERIC_MAX_DISPLAY_SCALE = 1000;
36
51
  /** the decimal digits in one base-10000 group (numeric.c's DEC_DIGITS, NBASE = 10^DEC_DIGITS) */
37
52
  const DEC_DIGITS = 4;
38
- 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);
39
57
  const abs = (x) => x < 0n ? -x : x;
40
58
  /** `x / d` rounded half away from zero (d > 0): how numeric.c's `round_var` and `div_var(…, round = true)` round */
41
59
  function divRound(x, d) {
@@ -47,12 +65,20 @@ function divRound(x, d) {
47
65
  /** digits, an underscore allowed between two of them (Postgres 16: `1_000`) */
48
66
  const DIGITS = String.raw `\d+(?:_\d+)*`;
49
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+))?$/;
50
70
  /** Postgres 16's non-decimal integers: `0x1F`, `0o17`, `0b101`, an underscore before any digit (`0x_1F`) */
51
71
  const NON_DECIMAL = /^\s*([+-])?0(?:[xX]((?:_?[0-9a-fA-F])+)|[oO]((?:_?[0-7])+)|[bB]((?:_?[01])+))\s*$/;
52
72
  /** A numeric from its text, as `numeric_in` reads it (held to Postgres by tests/pg-numeric.test.ts): the scale is the
53
73
  * decimals written, less the exponent (never below 0): `1.50` has scale 2, `1.5e3` is 1500 with scale 0, `1e-3` is
54
74
  * 0.001 with scale 3; `1_000` is 1000 and `0x1F` is 31 (scale 0). NaN and Infinity are no PgNumeric: refused here. */
55
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
+ }
56
82
  const x = NON_DECIMAL.exec(text);
57
83
  if (x !== null) {
58
84
  const [prefix, digits] = x[2] !== undefined ? ["0x", x[2]] : x[3] !== undefined ? ["0o", x[3]] : ["0b", x[4]];
@@ -89,6 +115,121 @@ export function numericText(n) {
89
115
  const text = n.scale === 0 ? whole : `${whole}.${digits.slice(digits.length - n.scale)}`;
90
116
  return n.unscaled < 0n ? `-${text}` : text;
91
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
+ }
92
233
  /** The value at a larger scale (more trailing zeros), unchanged. */
93
234
  const atScale = (n, scale) => n.unscaled * pow10(scale - n.scale);
94
235
  export const add = (a, b) => {
@@ -119,15 +260,26 @@ function groups(n) {
119
260
  const digits = lsFirst.slice(low).reverse();
120
261
  return { digits, weight: lsFirst.length - 1 - fracGroups };
121
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
+ }
122
276
  /**
123
277
  * `select_div_scale` (numeric.c), faithfully: a quotient keeps at least NUMERIC_MIN_SIG_DIGITS significant digits —
124
278
  * the scale is read from the WEIGHTS and FIRST GROUPS of the two values, so it depends on the values, not only their
125
279
  * types — and never fewer decimals than either input shows, between 0 and NUMERIC_MAX_DISPLAY_SCALE.
126
280
  */
127
281
  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;
282
+ const [weight1, first1] = leadGroup(a), [weight2, first2] = leadGroup(b);
131
283
  // the quotient's weight; when the first groups are equal it may be either, and the smaller is assumed
132
284
  let qweight = weight1 - weight2;
133
285
  if (first1 <= first2)
@@ -239,7 +391,8 @@ export function withTypmod(a, precision, scale) {
239
391
  const maxDigits = precision - scale;
240
392
  const digits = r.unscaled === 0n ? null : abs(r.unscaled).toString().length - r.scale;
241
393
  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");
394
+ const o = fieldOverflow(precision, scale);
395
+ throw new NumericError(o.message, "22003", null, o.detail);
243
396
  }
244
397
  return r;
245
398
  }
@@ -334,16 +487,46 @@ export function aggregateType(agg, arg) {
334
487
  case "max": return arg;
335
488
  }
336
489
  }
337
- /** 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. */
338
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) {
339
518
  switch (p.op) {
340
- case "cell": return [p.key];
519
+ case "cell":
341
520
  case "literal": return [];
342
521
  case "add":
343
522
  case "sub":
344
523
  case "mul":
345
524
  case "div":
346
- 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;
347
530
  case "neg":
348
531
  case "round":
349
532
  case "typmod":
@@ -351,45 +534,61 @@ export function cellsOf(p) {
351
534
  case "abs":
352
535
  case "floor":
353
536
  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)];
537
+ case "pad": return [p.arg];
538
+ case "pick": return [p.index, ...p.args];
539
+ case "rescale": return [p.arg, p.scale];
361
540
  }
362
541
  }
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) {
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);
365
556
  switch (p.op) {
366
557
  case "cell": {
367
558
  const v = row[p.key];
368
559
  return v === null || v === undefined ? null : numericOf(v);
369
560
  }
370
- 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
+ }
371
570
  case "neg": {
372
- const a = evaluate(p.arg, row);
571
+ const a = ev(p.arg);
373
572
  return a === null ? null : { unscaled: -a.unscaled, scale: a.scale };
374
573
  }
375
574
  case "round": {
376
- const a = evaluate(p.arg, row);
575
+ const a = ev(p.arg);
377
576
  return a === null ? null : round(a, p.scale);
378
577
  }
379
578
  case "typmod": {
380
- const a = evaluate(p.arg, row);
579
+ const a = ev(p.arg);
381
580
  return a === null ? null : withTypmod(a, p.precision, p.scale);
382
581
  }
383
582
  case "trunc": {
384
- const a = evaluate(p.arg, row);
583
+ const a = ev(p.arg);
385
584
  return a === null ? null : trunc(a, p.scale);
386
585
  }
387
586
  case "pad": {
388
- const a = evaluate(p.arg, row);
587
+ const a = ev(p.arg);
389
588
  return a === null || a.scale >= p.scale ? a : { unscaled: a.unscaled * pow10(p.scale - a.scale), scale: p.scale };
390
589
  }
391
590
  case "rescale": {
392
- const a = evaluate(p.arg, row), s = evaluate(p.scale, row);
591
+ const a = ev(p.arg), s = ev(p.scale);
393
592
  if (a === null || s === null || s.scale !== 0 || s.unscaled < 0n)
394
593
  return a;
395
594
  const to = Number(s.unscaled);
@@ -399,20 +598,20 @@ export function evaluate(p, row) {
399
598
  return a.unscaled % by === 0n ? { unscaled: a.unscaled / by, scale: to } : a;
400
599
  }
401
600
  case "abs": {
402
- const a = evaluate(p.arg, row);
601
+ const a = ev(p.arg);
403
602
  return a === null ? null : { unscaled: abs(a.unscaled), scale: a.scale };
404
603
  }
405
604
  case "floor": {
406
- const a = evaluate(p.arg, row);
605
+ const a = ev(p.arg);
407
606
  return a === null ? null : floor(a);
408
607
  }
409
608
  case "ceil": {
410
- const a = evaluate(p.arg, row);
609
+ const a = ev(p.arg);
411
610
  return a === null ? null : ceil(a);
412
611
  }
413
612
  case "coalesce": {
414
613
  for (const x of p.args) {
415
- const v = evaluate(x, row);
614
+ const v = ev(x);
416
615
  if (v !== null)
417
616
  return v;
418
617
  }
@@ -422,68 +621,210 @@ export function evaluate(p, row) {
422
621
  case "least": {
423
622
  let best = null;
424
623
  for (const x of p.args) {
425
- const v = evaluate(x, row);
624
+ const v = ev(x);
426
625
  if (v !== null && (best === null || (p.op === "greatest" ? compare(v, best) > 0 : compare(v, best) < 0)))
427
626
  best = v;
428
627
  }
429
628
  return best;
430
629
  }
431
630
  case "mod": {
432
- 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]);
433
632
  return a === null || b === null ? null : mod(a, b);
434
633
  }
435
634
  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);
635
+ const arm = takenArm(p, ev);
636
+ return arm === undefined ? null : ev(arm);
439
637
  }
440
638
  case "stddev": {
441
- const [n, x, x2] = p.args.map((a) => evaluate(a, row));
639
+ const [n, x, x2] = p.args.map(ev);
442
640
  return n === null || n === undefined || x === null || x === undefined || x2 === null || x2 === undefined ? null : stddev(n, x, x2, p.sample, p.variance);
443
641
  }
444
642
  default: {
445
- 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]);
446
644
  if (a === null || b === null)
447
645
  return null;
448
646
  return p.op === "add" ? add(a, b) : p.op === "sub" ? sub(a, b) : p.op === "mul" ? mul(a, b) : div(a, b);
449
647
  }
450
648
  }
451
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
+ }
452
655
  /** a numeric's special values, as numeric_out writes them (a text read by numeric_in may be one) */
453
656
  const SPECIAL = /^(NaN|-?Infinity)$/;
454
657
  /** 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) {
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) {
457
668
  if (p.op === "cell") {
458
669
  const v = row[p.key];
459
670
  return typeof v === "string" && SPECIAL.test(v) ? v : undefined;
460
671
  }
461
672
  if (p.op === "pad" || p.op === "rescale")
462
- return specialOf(p.arg, row);
673
+ return specialOf(p.arg, row, memo, seen);
463
674
  // the arm the row took (a special value in another is never read, as Postgres computes only the taken arm)
464
675
  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);
676
+ const arm = takenArm(p, (x) => evaluate(x, row, memo));
677
+ return arm === undefined ? undefined : specialOf(arm, row, memo, seen);
468
678
  }
469
679
  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");
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
+ }
473
685
  return v;
474
686
  }
475
687
  for (const key of cellsOf(p)) {
476
688
  const v = row[key];
477
689
  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");
690
+ throw new NumericError(`arithmetic on a numeric NaN or Infinity is not supported by Mason (${gapMark("numeric-special")})`, "0A000", "numeric-special");
479
691
  }
480
692
  return undefined;
481
693
  }
482
- /** 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. */
483
695
  export const evaluateText = (p, row) => {
484
- const special = specialOf(p, row);
696
+ const memo = new Map();
697
+ const special = specialOf(p, row, memo, new Map());
485
698
  if (special !== undefined)
486
699
  return special;
487
- const v = evaluate(p, row);
700
+ const v = evaluate(p, row, memo);
488
701
  return v === null ? null : numericText(v);
489
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
+ }