@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,764 @@
1
+ /**
2
+ * POSTGRES'S FUNCTIONS WHERE DUCKDB'S ANSWER DIFFERENTLY (docs/DESIGN.md "Postgres decides, DuckDB computes"):
3
+ * what the rewrite (src/duck/rewrite.ts) calls in place of a DuckDB function that gives another answer, or of a
4
+ * Postgres function DuckDB has none of. Two kinds:
5
+ * - SCALAR FUNCTIONS in JavaScript (`registerFunctions`, once per DuckDB instance: a registered function is the
6
+ * instance's, every connection sees it). JSON is the main one: DuckDB's JSON functions parse a value and write it
7
+ * again (`7.00` comes back `7.0`, `{"a" : 1}` as `{"a":1}`), where Postgres's `json` keeps the text it was given and
8
+ * `jsonb` writes it canonically (src/duck/jsonb.ts) — so every read of a json value is done here, on its text;
9
+ * - and the array functions a lambda would build from DuckDB's (the deparser cannot print a lambda, and DuckDB 2.0
10
+ * binds a lambda MACRO's constant arguments wrongly once its postgres extension is loaded: no macro is used).
11
+ * Every name is `__mason_*`, which no Postgres function has.
12
+ */
13
+ import { BIGINT, BOOLEAN, DATE, DOUBLE, DuckDBDateValue, DuckDBIntervalValue, DuckDBScalarFunction, DuckDBTimestampTZValue, DuckDBTimestampValue, INTEGER, INTERVAL, LIST, TIMESTAMP, TIMESTAMPTZ, VARCHAR, listValue } from "@duckdb/node-api";
14
+ import { pgEvalSync, PgEvalError } from "./pg-eval.js";
15
+ import { libcKept, libcKey } from "./libc-order.js";
16
+ import { formatFloat } from "../api/postgres/encode.js";
17
+ import { floatAsNumeric } from "./types.js";
18
+ import { intervalText } from "./types.js";
19
+ import { jsonbText, parseJson, writeJson, quoteJson } from "./jsonb.js";
20
+ import { compare, div, evaluateText, NumericError, numericText, parseNumeric } from "../lib/pg-numeric.js";
21
+ import { toCharNumber, toCharTime } from "./to-char.js";
22
+ import { atTimeZoneOf } from "../lib/timezone.js";
23
+ import { gapMark } from "./gaps.js";
24
+ import { CANCEL_SLOTS, cancelTable, isCancelled, slotOf } from "./cancel.js";
25
+ // ------------------------------------------------------------------------------------------------- case mapping
26
+ /** one character's simple mapping, as towupper / towlower give it: a character whose full mapping is several (`ß`,
27
+ * `ʼn`) keeps itself */
28
+ function simple(ch, upper) {
29
+ const m = upper ? ch.toUpperCase() : ch.toLowerCase();
30
+ return [...m].length === 1 ? m : ch;
31
+ }
32
+ const ASCII_ALNUM = /^[A-Za-z0-9]$/;
33
+ const ALNUM = /^[\p{L}\p{N}]$/u;
34
+ /** upper() / lower() as Postgres computes them under that mode (libc's upper is the rewrite's SQL, not this) */
35
+ export function pgCase(s, upper, mode) {
36
+ if (mode === "icu")
37
+ return upper ? s.toUpperCase() : s.toLowerCase();
38
+ if (mode === "ascii")
39
+ return s.replace(upper ? /[a-z]+/g : /[A-Z]+/g, (x) => upper ? x.toUpperCase() : x.toLowerCase());
40
+ let out = "";
41
+ for (const ch of s)
42
+ out += simple(ch, upper);
43
+ return out;
44
+ }
45
+ /** initcap(): a letter after a letter or a digit is lowered, every other is raised (Postgres's str_initcap) */
46
+ export function initcap(s, mode) {
47
+ let out = "", was = false;
48
+ for (const ch of s) {
49
+ if (mode === "ascii")
50
+ out += was ? ch.replace(/[A-Z]/, (c) => c.toLowerCase()) : ch.replace(/[a-z]/, (c) => c.toUpperCase());
51
+ else if (mode === "icu")
52
+ out += was ? ch.toLowerCase() : ch.toUpperCase();
53
+ else
54
+ out += simple(ch, !was);
55
+ was = (mode === "ascii" ? ASCII_ALNUM : ALNUM).test(ch);
56
+ }
57
+ return out;
58
+ }
59
+ /**
60
+ * A `#>` PATH ELEMENT AS AN ARRAY INDEX, as Postgres reads one (jsonfuncs.c get_path_all and jsonb_get_element: `strtoint`
61
+ * in base 10, nothing after the digits): white space before it, a sign (`+` or `-`), digits — `' 1'` and `'+1'` are 1,
62
+ * `'1 '` is no index (on an array its read is NULL; on an object every element is a key); a number outside int4 is none.
63
+ * null where the element is not an index. THE ONE DEFINITION: the function's step reads it, and the native read
64
+ * (src/duck/rewrite.ts jsonGet) leaves to the function every element that is one (tests/conventions.test.ts).
65
+ */
66
+ export function pathIndex(element) {
67
+ if (!/^[ \t\n\v\f\r]*[+-]?[0-9]+$/.test(element))
68
+ return null;
69
+ const n = Number(element.trim());
70
+ return n >= -2147483648 && n <= 2147483647 ? n : null;
71
+ }
72
+ function step(v, s) {
73
+ if ("key" in s || ("either" in s && v.t === "obj")) {
74
+ if (v.t !== "obj")
75
+ return null;
76
+ const k = "key" in s ? s.key : s.either;
77
+ // a duplicate key: its LAST value (jsonb keeps only that one; json's -> reads the last)
78
+ for (let i = v.v.length - 1; i >= 0; i--)
79
+ if (v.v[i][0] === k)
80
+ return v.v[i][1];
81
+ return null;
82
+ }
83
+ if (v.t !== "arr")
84
+ return null;
85
+ let i;
86
+ if ("index" in s)
87
+ i = s.index;
88
+ else {
89
+ const n = pathIndex(s.either);
90
+ if (n === null)
91
+ return null;
92
+ i = n;
93
+ }
94
+ if (i < 0)
95
+ i += v.v.length;
96
+ return i >= 0 && i < v.v.length ? v.v[i] : null;
97
+ }
98
+ /** a json value as text: `->>`'s (a string unquoted, a JSON null is SQL NULL), else its json text — jsonb's
99
+ * canonical one, or json's as the document has it */
100
+ function valueText(source, v, canonical, asText) {
101
+ if (asText) {
102
+ if (v.t === "null")
103
+ return null;
104
+ if (v.t === "str")
105
+ return v.v;
106
+ if (v.t === "num" && canonical)
107
+ return numericText(parseNumeric(v.text));
108
+ }
109
+ return canonical ? writeJson(v) : source.slice(v.start, v.end);
110
+ }
111
+ /** `->` / `->>` / `#>` / `#>>` / json(b)_extract_path(_text): the value at the steps, or null */
112
+ export function jsonGet(text, steps, canonical, asText) {
113
+ let v = parseJson(text);
114
+ for (const s of steps) {
115
+ // a jsonb scalar is stored as a one-element array: `'"s"'::jsonb -> 0` (and `-> -1`) is the scalar itself
116
+ if (canonical && "index" in s && v.t !== "arr" && v.t !== "obj" && (s.index === 0 || s.index === -1))
117
+ continue;
118
+ v = step(v, s);
119
+ if (v === null)
120
+ return null;
121
+ }
122
+ return valueText(text, v, canonical, asText);
123
+ }
124
+ /**
125
+ * A STORED JSON PATH'S VALUE (src/duck/kept.ts): what `jsonGet` answers for the path's `->` read (the value's JSON
126
+ * text), computed once when the row is written, `KEPT_NULL` where that answer is NULL, or `KEPT_RECOMPUTE` where it
127
+ * cannot stand in for the read — `jsonGet` raised (the read raises it again, as the statement without the stored value
128
+ * would), or the answer begins with a NUL (never a JSON text's first character; kept for any other caller). The
129
+ * column's own NULL is a row not stored yet (its default: an ADD COLUMN that writes no value, src/duck/kept.ts `keep`),
130
+ * so a NULL answer is the mark, and SQL turns a NULL document's answer into it (`keptValueSql`). Never throws: a write
131
+ * of the copy (the stream's, the poll's) must not fail over one row's JSON.
132
+ */
133
+ export const KEPT_NULL = "\u0000";
134
+ export const KEPT_RECOMPUTE = "\u0000!";
135
+ export function keptJsonValue(text, steps, canonical, asText) {
136
+ let v;
137
+ try {
138
+ v = jsonGet(text, steps, canonical, asText);
139
+ }
140
+ catch {
141
+ return KEPT_RECOMPUTE;
142
+ } // the read raises it, with its own words: nothing to store but the mark
143
+ return v === null ? KEPT_NULL : v.startsWith(KEPT_NULL) ? KEPT_RECOMPUTE : v;
144
+ }
145
+ /** json(b)_array_elements(_text): each element's text; an array only (Postgres refuses a scalar or an object) */
146
+ export function jsonElements(text, canonical, asText) {
147
+ const v = parseJson(text);
148
+ if (v.t !== "arr")
149
+ throw new Error(v.t === "obj" ? "cannot extract elements from an object" : "cannot extract elements from a scalar");
150
+ return v.v.map((x) => valueText(text, x, canonical, asText));
151
+ }
152
+ /** json(b)_each(_text): each member's key and value text, in the document's order (jsonb's is its canonical one) */
153
+ export function jsonMembers(text, canonical, asText) {
154
+ const v = parseJson(canonical ? jsonbText(text) : text);
155
+ if (v.t !== "obj")
156
+ throw new Error(v.t === "arr" ? "cannot call json_each on an array" : "cannot deconstruct a scalar");
157
+ const src = canonical ? jsonbText(text) : text;
158
+ return v.v.map(([k, x]) => [k, valueText(src, x, canonical, asText)]);
159
+ }
160
+ /** jsonb's `-` of a key: an object without it, an array without its string elements equal to it */
161
+ export function jsonbDelete(text, key) {
162
+ const v = parseJson(text);
163
+ if (v.t === "obj")
164
+ return writeJson({ t: "obj", v: v.v.filter(([k]) => k !== key) });
165
+ if (v.t === "arr")
166
+ return writeJson({ t: "arr", v: v.v.filter((x) => !(x.t === "str" && x.v === key)) });
167
+ throw new Error("cannot delete from scalar");
168
+ }
169
+ /** jsonb's `||`: two objects merged (the right's keys win), else both as arrays, concatenated (jsonb_concat) */
170
+ export function jsonbConcat(a, b) {
171
+ const x = parseJson(a), y = parseJson(b);
172
+ if (x.t === "obj" && y.t === "obj")
173
+ return writeJson({ t: "obj", v: [...x.v, ...y.v] });
174
+ const items = (j) => j.t === "arr" ? j.v : [j];
175
+ return writeJson({ t: "arr", v: [...items(x), ...items(y)] });
176
+ }
177
+ /** jsonb_strip_nulls: every object member whose value is null, removed, at every depth (an array's nulls stay) */
178
+ export function jsonbStripNulls(text) {
179
+ const strip = (j) => j.t === "obj" ? { t: "obj", v: j.v.filter(([, x]) => x.t !== "null").map(([k, x]) => [k, strip(x)]) }
180
+ : j.t === "arr" ? { t: "arr", v: j.v.map(strip) } : j;
181
+ return writeJson(strip(parseJson(text)));
182
+ }
183
+ /** quote_literal: the text as a string literal, `E'…'` with each backslash doubled where it holds one */
184
+ /**
185
+ * A float cast to numeric, as Postgres writes the numeric (float8_numeric: `%.15g` of the value, float4_numeric
186
+ * `%.6g`, read by numeric_in): `25`, never `25.0`; `20000001.279999997` is `20000001.28`; a -0 is `0`; NaN and the
187
+ * infinities are numeric's. The wire's `floatAsNumeric` (src/duck/types.ts) is the one rule.
188
+ */
189
+ export function floatNumericText(x, float4) {
190
+ return floatAsNumeric(float4 ? Math.fround(x) : x, float4 ? 6 : 15);
191
+ }
192
+ export function quoteLiteral(text) {
193
+ const q = text.replaceAll("'", "''");
194
+ return text.includes("\\") ? `E'${q.replaceAll("\\", "\\\\")}'` : `'${q}'`;
195
+ }
196
+ /** array_to_string: the elements' text joined, a NULL element skipped, or written as `nulls` when it is given */
197
+ export function arrayToString(items, sep, nulls) {
198
+ return items.flatMap((x) => x !== null ? [x] : nulls !== null ? [nulls] : []).join(sep);
199
+ }
200
+ // ------------------------------------------------------------------------------------------------- intervals
201
+ /**
202
+ * extract(field FROM interval), as Postgres 18 computes it (interval_part_common): the interval's parts each truncated
203
+ * toward zero — years and months of its months, its days, hours, minutes, seconds and micros of its time — as numeric
204
+ * text (a second with 6 decimals, a millisecond with 3, an epoch with 6: a year is 365.25 days, a month 30).
205
+ */
206
+ export function intervalPart(field, months, days, micros) {
207
+ const year = Math.trunc(months / 12), mon = months % 12;
208
+ const hour = micros / 3600000000n, minute = (micros / 60000000n) % 60n, usec = micros % 60000000n;
209
+ const fixed = (v, scale) => {
210
+ const neg = v < 0n, a = neg ? -v : v, d = 10n ** BigInt(scale);
211
+ const text = scale === 0 ? `${a}` : `${a / d}.${`${a % d}`.padStart(scale, "0")}`;
212
+ return neg ? `-${text}` : text;
213
+ };
214
+ switch (field) {
215
+ case "microsecond":
216
+ case "microseconds": return fixed(usec, 0);
217
+ case "millisecond":
218
+ case "milliseconds": return fixed(usec, 3);
219
+ case "second":
220
+ case "seconds": return fixed(usec, 6);
221
+ case "minute":
222
+ case "minutes": return `${minute}`;
223
+ case "hour":
224
+ case "hours": return `${hour}`;
225
+ case "day":
226
+ case "days": return `${days}`;
227
+ case "week":
228
+ case "weeks": return `${Math.trunc(days / 7)}`;
229
+ case "month":
230
+ case "months": return `${mon}`;
231
+ case "quarter": return `${months >= 0 ? Math.trunc(mon / 3) + 1 : -(Math.trunc(-mon / 3) + 1)}`;
232
+ case "year":
233
+ case "years": return `${year}`;
234
+ case "decade":
235
+ case "decades": return `${Math.trunc(year / 10)}`;
236
+ case "century":
237
+ case "centuries": return `${Math.trunc(year / 100)}`;
238
+ case "millennium":
239
+ case "millennia": return `${Math.trunc(year / 1000)}`;
240
+ case "epoch":
241
+ return fixed(micros + (31557600n * BigInt(year) + 2592000n * BigInt(mon) + 86400n * BigInt(days)) * 1000000n, 6);
242
+ default: throw new Error(`unit "${field}" not supported for type interval`);
243
+ }
244
+ }
245
+ /** justify_days / justify_hours / justify_interval (Postgres's interval_justify_*): 30 days a month, 24 hours a day,
246
+ * the parts given one sign */
247
+ export function justify(kind, months, days, micros) {
248
+ let m = months, d = days, t = micros;
249
+ if (kind === "hours" || kind === "interval") {
250
+ d += Number(t / 86400000000n);
251
+ t %= 86400000000n;
252
+ }
253
+ if (kind === "days" || kind === "interval") {
254
+ m += Math.trunc(d / 30);
255
+ d %= 30;
256
+ }
257
+ if (kind === "interval") {
258
+ if (m > 0 && (d < 0 || (d === 0 && t < 0))) {
259
+ d += 30;
260
+ m--;
261
+ }
262
+ else if (m < 0 && (d > 0 || (d === 0 && t > 0))) {
263
+ d -= 30;
264
+ m++;
265
+ }
266
+ }
267
+ if (kind === "days" && ((m > 0 && d < 0) || (m < 0 && d > 0))) {
268
+ d += m > 0 ? 30 : -30;
269
+ m += m > 0 ? -1 : 1;
270
+ }
271
+ if (kind === "hours" || kind === "interval") {
272
+ if (d > 0 && t < 0) {
273
+ t += 86400000000n;
274
+ d--;
275
+ }
276
+ else if (d < 0 && t > 0) {
277
+ t -= 86400000000n;
278
+ d++;
279
+ }
280
+ }
281
+ return [m, d, t];
282
+ }
283
+ /** DuckDB's text of a timestamp (`2026-01-01 10:00:00.5+01`) as Postgres's json writes it (`2026-01-01T10:00:00.5+01:00`) */
284
+ function isoTime(text, zoned) {
285
+ if (/infinity/i.test(text))
286
+ return text.toLowerCase();
287
+ const t = text.replace(" ", "T");
288
+ if (!zoned)
289
+ return t;
290
+ return t.replace(/([+-]\d{2})$/, "$1:00").replace(/([+-]\d{2})(\d{2})$/, "$1:$2");
291
+ }
292
+ export function jsonValue(kind, text) {
293
+ if (text === null)
294
+ return "null";
295
+ switch (kind) {
296
+ case "s": return quoteJson(text);
297
+ case "r": return text;
298
+ // a number: as written where JSON can carry it, a string where not (NaN, Infinity: datum_to_json's IsValidJsonNumber)
299
+ case "n": return /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/.test(text) ? text : quoteJson(text);
300
+ case "k": return jsonbText(text);
301
+ case "z":
302
+ case "t": return quoteJson(isoTime(text, kind === "z"));
303
+ }
304
+ }
305
+ /** an array as to_json / array_to_json write it (Postgres's array_dim_to_json): `[v,v]`, no space between elements, each
306
+ * written as its element type's json — a jsonb or json element embedded as json, never quoted as a string */
307
+ export function jsonArray(kind, items) {
308
+ return `[${items.map((x) => jsonValue(kind, x)).join(",")}]`;
309
+ }
310
+ /** json_build_object / json_build_array / to_json, written as Postgres's json functions write them: `{"k" : v, …}`,
311
+ * `[v, …]`, the value alone. `shape` is the style and then one kind per argument (`o:sr` an object of one string key
312
+ * and one raw value). */
313
+ export function jsonBuild(shape, args) {
314
+ const [style, kinds = ""] = shape.split(":");
315
+ if (kinds.length !== args.length)
316
+ throw new Error(`a json build of ${args.length} argument(s) was described as ${shape}: report it (Mason's rewrite)`);
317
+ const values = args.map((a, i) => jsonValue(kinds[i], a));
318
+ switch (style) {
319
+ // to_json is strict: NULL in, NULL out
320
+ case "v": return args[0] === null || args[0] === undefined ? null : values[0];
321
+ case "a": return `[${values.join(", ")}]`;
322
+ // a row as row_to_json writes it: no space anywhere, a NULL row is NULL
323
+ case "r": {
324
+ const members = [];
325
+ for (let i = 0; i < args.length; i += 2)
326
+ members.push(`${quoteJson(args[i])}:${values[i + 1]}`);
327
+ return `{${members.join(",")}}`;
328
+ }
329
+ case "o": {
330
+ if (args.length % 2 !== 0)
331
+ throw new Error("argument list must have even number of elements");
332
+ const members = [];
333
+ for (let i = 0; i < args.length; i += 2) {
334
+ if (args[i] === null)
335
+ throw new Error(`argument ${i + 1}: key must not be null`);
336
+ members.push(`${quoteJson(args[i])} : ${values[i + 1]}`);
337
+ }
338
+ return `{${members.join(", ")}}`;
339
+ }
340
+ default: throw new Error(`a json build style ${style} is unknown: report it (Mason's rewrite)`);
341
+ }
342
+ }
343
+ /** encode(bytea, format) as Postgres writes it: base64 in lines of 76, escape with a byte outside printable ASCII
344
+ * as its octal and a backslash doubled */
345
+ export function byteaEncode(bytes, format) {
346
+ if (format === "base64")
347
+ return (bytes.toString("base64").match(/.{1,76}/g) ?? []).join("\n");
348
+ let out = "";
349
+ for (const b of bytes)
350
+ out += b === 0x5c ? "\\\\" : b < 0x20 || b > 0x7e ? `\\${b.toString(8).padStart(3, "0")}` : String.fromCharCode(b);
351
+ return out;
352
+ }
353
+ /** decode(text, format), Postgres's readings and its errors (22023) */
354
+ export function byteaDecode(text, format) {
355
+ if (format === "hex") {
356
+ const digits = text.replace(/[ \t\n\r]/g, "");
357
+ const bad = /[^0-9a-fA-F]/.exec(digits);
358
+ if (bad !== null)
359
+ throw new PgEvalError("22023", `invalid hexadecimal digit: "${bad[0]}"`);
360
+ if (digits.length % 2 !== 0)
361
+ throw new PgEvalError("22023", "invalid hexadecimal data: odd number of digits");
362
+ return Buffer.from(digits, "hex");
363
+ }
364
+ if (format === "base64") {
365
+ const s = text.replace(/[ \t\n\r]/g, "");
366
+ const bad = /[^A-Za-z0-9+/=]/.exec(s);
367
+ if (bad !== null)
368
+ throw new PgEvalError("22023", `invalid symbol "${bad[0]}" found while decoding base64 sequence`);
369
+ if (/=[^=]/.test(s) || s.length % 4 !== 0)
370
+ throw new PgEvalError("22023", "invalid base64 end sequence");
371
+ return Buffer.from(s, "base64");
372
+ }
373
+ // escape: `\\` a backslash, `\ooo` an octal byte, anything else its own UTF-8 bytes
374
+ const out = [];
375
+ const bytes = Buffer.from(text, "utf8");
376
+ for (let i = 0; i < bytes.length; i++) {
377
+ if (bytes[i] !== 0x5c) {
378
+ out.push(bytes[i]);
379
+ continue;
380
+ }
381
+ if (bytes[i + 1] === 0x5c) {
382
+ out.push(0x5c);
383
+ i++;
384
+ continue;
385
+ }
386
+ const oct = bytes.subarray(i + 1, i + 4).toString("latin1");
387
+ if (!/^[0-3][0-7][0-7]$/.test(oct))
388
+ throw new PgEvalError("22P02", "invalid input syntax for type bytea");
389
+ out.push(parseInt(oct, 8));
390
+ i += 3;
391
+ }
392
+ return Buffer.from(out);
393
+ }
394
+ /** A row's text as record_out writes it: `(1,"a b",,t)`, a column quoted when it is empty or holds a quote, a
395
+ * backslash, a parenthesis, a comma or white space (a quote and a backslash doubled inside), NULL as nothing. */
396
+ export function recordText(cols) {
397
+ return `(${cols.map((c) => c === null ? "" : c === "" || /["\\(),\s]/.test(c) ? `"${c.replace(/(["\\])/g, "$1$1")}"` : c).join(",")})`;
398
+ }
399
+ // ------------------------------------------------------------------------------------------------- arrays
400
+ /** An array's text as array_out writes it: `{a,"b c",NULL}`, an element quoted when it is empty, NULL's word, or holds
401
+ * a brace, a comma, a quote, a backslash or white space. `bool` elements are `t` and `f`. */
402
+ export function arrayText(items, bool) {
403
+ return `{${items.map((x) => {
404
+ if (x === null)
405
+ return "NULL";
406
+ const t = bool ? (x === "true" ? "t" : x === "false" ? "f" : x) : x;
407
+ return t === "" || /^null$/i.test(t) || /[{},"\\\s]/.test(t) ? `"${t.replace(/["\\]/g, (c) => `\\${c}`)}"` : t;
408
+ }).join(",")}}`;
409
+ }
410
+ // ------------------------------------------------------------------------------------------------- jsonb's tests
411
+ /** a jsonb document at its root as Postgres's containment reads it: a scalar is an array of one (JB_ROOT_IS_SCALAR) */
412
+ const root = (j) => j.t === "arr" || j.t === "obj" ? j : { t: "arr", v: [j] };
413
+ /** two jsonb scalars equal as equalsJsonbScalarValue says: a number by its value (1.0 = 1), the rest exactly */
414
+ function sameScalar(a, b) {
415
+ if (a.t !== b.t)
416
+ return false;
417
+ switch (a.t) {
418
+ case "null": return true;
419
+ case "bool": return a.v === b.v;
420
+ case "str": return a.v === b.v;
421
+ case "num": return compare(parseNumeric(a.text), parseNumeric(b.text)) === 0;
422
+ default: return false;
423
+ }
424
+ }
425
+ /** JsonbDeepContains: an object holds each of the template's keys with a value that contains the template's; an array
426
+ * holds, for each template element, an equal scalar or a container that contains it — of the same kind */
427
+ function contains(a, b) {
428
+ if (a.t === "obj" && b.t === "obj") {
429
+ return b.v.every(([k, bv]) => {
430
+ // a duplicate key reads its last value, as jsonb keeps it
431
+ const av = [...a.v].reverse().find(([ak]) => ak === k)?.[1];
432
+ if (av === undefined)
433
+ return false;
434
+ if ((av.t === "obj" || av.t === "arr") !== (bv.t === "obj" || bv.t === "arr"))
435
+ return false;
436
+ return av.t === "obj" || av.t === "arr" ? av.t === bv.t && contains(av, bv) : sameScalar(av, bv);
437
+ });
438
+ }
439
+ if (a.t === "arr" && b.t === "arr") {
440
+ return b.v.every((bv) => bv.t === "obj" || bv.t === "arr"
441
+ ? a.v.some((av) => av.t === bv.t && contains(av, bv))
442
+ : a.v.some((av) => sameScalar(av, bv)));
443
+ }
444
+ return false;
445
+ }
446
+ /** `a @> b` on jsonb (jsonb_contains): their roots must both be objects or both not, then JsonbDeepContains */
447
+ export function jsonbContains(a, b) {
448
+ const x = root(parseJson(a)), y = root(parseJson(b));
449
+ return (x.t === "obj") === (y.t === "obj") && contains(x, y);
450
+ }
451
+ /** `j ? key` on jsonb (jsonb_exists): an object's top-level key, or a string element of an array (a string scalar's own) */
452
+ export function jsonbExists(j, key) {
453
+ const x = root(parseJson(j));
454
+ return x.t === "obj" ? x.v.some(([k]) => k === key) : x.t === "arr" && x.v.some((e) => e.t === "str" && e.v === key);
455
+ }
456
+ /** json(b)_array_length: an array's element count, Postgres's error for anything else */
457
+ export function jsonArrayLength(j) {
458
+ const x = parseJson(j);
459
+ if (x.t === "arr")
460
+ return x.v.length;
461
+ throw new Error(x.t === "obj" ? "cannot get array length of a non-array" : "cannot get array length of a scalar");
462
+ }
463
+ /** string_to_array(text, delimiter [, null_string]) as Postgres splits: '' is no element, a NULL delimiter splits into
464
+ * characters, an empty one keeps the text whole, and an element equal to null_string is NULL */
465
+ export function stringToArray(s, delimiter, nullString) {
466
+ if (s === "")
467
+ return [];
468
+ const parts = delimiter === null ? [...s] : delimiter === "" ? [s] : s.split(delimiter);
469
+ return parts.map((p) => nullString !== null && p === nullString ? null : p);
470
+ }
471
+ const itemsOf = (v) => v.items.map((x) => x === null ? null : String(x));
472
+ // ------------------------------------------------------------------------------------------------- registration
473
+ const text = (v) => v === null || v === undefined ? null : String(v);
474
+ /** a scalar function over VARCHAR (and BOOLEAN) arguments, one call per row; a thrown error is the statement's */
475
+ function scalar(conn, name, params, returns, body, opts = {}) {
476
+ conn.registerScalarFunction(DuckDBScalarFunction.create({
477
+ name, returnType: returns,
478
+ // the session first (`cancelled` above), then the function's own arguments
479
+ ...(opts.varargs ? { varArgsType: VARCHAR } : { parameterTypes: [VARCHAR, ...params] }),
480
+ // every NULL is handled here: the session's is a connection without the variable, and a NULL argument is the
481
+ // function's to decide under `nulls: "pass"` (json_build_object writes `null`), else the answer is NULL
482
+ specialHandling: true,
483
+ mainFunction: (info, input, out) => {
484
+ FUNCTION_CALLS.set(name, (FUNCTION_CALLS.get(name) ?? 0) + 1);
485
+ const all = Array.from({ length: input.columnCount }, (_, c) => input.getColumnVector(c));
486
+ if (cancelled(info, all, input.rowCount))
487
+ return;
488
+ const vectors = all.slice(1);
489
+ try {
490
+ for (let r = 0; r < input.rowCount; r++) {
491
+ const args = vectors.map((v) => v.getItem(r));
492
+ out.setItem(r, opts.nulls !== "pass" && args.some((a) => a === null) ? null : body(args));
493
+ }
494
+ out.flush();
495
+ }
496
+ catch (e) {
497
+ // Postgres's own message where the function raises one (src/duck/store.ts postgresWording reads the prefix), and
498
+ // its SQLSTATE where the function says it
499
+ info.setError(e instanceof PgEvalError ? `mason[${e.code}]: ${e.message}` : `mason: ${e.message}`);
500
+ }
501
+ },
502
+ }));
503
+ }
504
+ // ------------------------------------------------------------------- Postgres's own functions (src/duck/pg-eval.ts)
505
+ /** what each `__mason_pg_*` function's answer is, and how the worker's Postgres writes it for DuckDB to take whole: a
506
+ * time as its microseconds (days, for a date), an interval as its three fields, ±infinity by name */
507
+ export const PG_KINDS = {
508
+ text: { returns: VARCHAR, sql: (x) => `(${x})::text`, read: (t) => t },
509
+ bool: { returns: BOOLEAN, sql: (x) => `(${x})::text`, read: (t) => t === "true" },
510
+ int8: { returns: BIGINT, sql: (x) => `(${x})::int8::text`, read: (t) => BigInt(t) },
511
+ date: {
512
+ returns: DATE, sql: (x) => `CASE WHEN isfinite(${x}) THEN (${x} - date '1970-01-01')::text ELSE (${x})::text END`,
513
+ read: (t) => t === "infinity" ? DuckDBDateValue.PosInf : t === "-infinity" ? DuckDBDateValue.NegInf : new DuckDBDateValue(Number(t)),
514
+ },
515
+ timestamp: {
516
+ returns: TIMESTAMP, sql: (x) => `CASE WHEN isfinite(${x}) THEN (extract(epoch FROM ${x}) * 1000000)::int8::text ELSE (${x})::text END`,
517
+ read: (t) => t === "infinity" ? DuckDBTimestampValue.PosInf : t === "-infinity" ? DuckDBTimestampValue.NegInf : new DuckDBTimestampValue(BigInt(t)),
518
+ },
519
+ timestamptz: {
520
+ returns: TIMESTAMPTZ, sql: (x) => `CASE WHEN isfinite(${x}) THEN (extract(epoch FROM ${x}) * 1000000)::int8::text ELSE (${x})::text END`,
521
+ read: (t) => t === "infinity" ? DuckDBTimestampTZValue.PosInf : t === "-infinity" ? DuckDBTimestampTZValue.NegInf : new DuckDBTimestampTZValue(BigInt(t)),
522
+ },
523
+ // an infinite interval (Postgres 17+) has no DuckDB value: refused by name where one is met
524
+ interval: {
525
+ returns: INTERVAL,
526
+ sql: (x) => `CASE WHEN isfinite(${x}) THEN concat_ws(' ', (extract(year FROM ${x}) * 12 + extract(month FROM ${x}))::int, extract(day FROM ${x})::int, ` +
527
+ `(extract(hour FROM ${x}) * 3600000000 + extract(minute FROM ${x}) * 60000000 + extract(microseconds FROM ${x}))::int8) ELSE (${x})::text END`,
528
+ read: (t) => {
529
+ const parts = t.split(" ");
530
+ if (parts.length !== 3)
531
+ throw new PgEvalError("0A000", `an infinite interval (${t}) is not supported by Mason (Mason gap infinite-interval)`);
532
+ return new DuckDBIntervalValue(Number(parts[0]), Number(parts[1]), BigInt(parts[2]));
533
+ },
534
+ },
535
+ list: { returns: LIST(VARCHAR), sql: (x) => `(${x})::text[]`, read: (t) => listValue(JSON.parse(t)) },
536
+ };
537
+ /** a relative time read from a value at run time means the transaction's start, which the worker's Postgres does not
538
+ * share: refused by name (a literal is the session's, src/duck/rewrite.ts) */
539
+ const RELATIVE = /\b(now|today|tomorrow|yesterday)\b/i;
540
+ const READS_TIME = /::\s*(date|timestamp|timestamptz|timestamp with(out)? time zone)\b/i;
541
+ /**
542
+ * Postgres's answer for a chunk: `template` an expression over a1…an (each the argument's text), applied by the
543
+ * worker's Postgres to the chunk's distinct argument tuples in one statement, under the zone DuckDB's session has. A
544
+ * NULL argument answers NULL (every function sent here is strict); Postgres's error is the statement's, in its SQLSTATE.
545
+ */
546
+ function viaPostgres(conn, kind) {
547
+ const k = PG_KINDS[kind];
548
+ conn.registerScalarFunction(DuckDBScalarFunction.create({
549
+ name: `__mason_pg_${kind}`, returnType: k.returns, varArgsType: VARCHAR,
550
+ mainFunction: (info, input, out) => {
551
+ try {
552
+ const all = Array.from({ length: input.columnCount }, (_, c) => input.getColumnVector(c));
553
+ if (cancelled(info, all, input.rowCount))
554
+ return;
555
+ const vectors = all.slice(1);
556
+ const rows = Array.from({ length: input.rowCount }, (_, r) => vectors.map((v) => text(v.getItem(r))));
557
+ // the zone and the template are the rewrite's constants, the same on every row; grouped by them all the same
558
+ const groups = new Map();
559
+ for (const [zone = null, template = null, ...args] of rows) {
560
+ if (zone === null || template === null || args.some((a) => a === null))
561
+ continue;
562
+ const g = groups.get(`${zone}\u0000${template}`) ?? { zone, template, tuples: new Map() };
563
+ groups.set(`${zone}\u0000${template}`, g);
564
+ g.tuples.set(JSON.stringify(args), args);
565
+ }
566
+ const answers = new Map();
567
+ for (const [key, g] of groups) {
568
+ const tuples = [...g.tuples.values()];
569
+ if (kind === "date" || kind === "timestamp" || kind === "timestamptz" || READS_TIME.test(g.template)) {
570
+ const relative = tuples.flat().find((a) => a !== null && RELATIVE.test(a));
571
+ if (relative !== undefined)
572
+ throw new PgEvalError("0A000", `the text '${relative}' read as a time is not supported by Mason: it means the transaction's start (Mason gap relative-time-literal)`);
573
+ }
574
+ const n = tuples[0].length;
575
+ const cols = Array.from({ length: n }, (_, i) => `a${i + 1}`);
576
+ const sql = `SELECT ${k.sql("mason_x")} FROM unnest(${cols.map((_, i) => `$${i + 1}::text[]`).join(", ")}) WITH ORDINALITY AS mason_u(${cols.join(", ")}, mason_n) ` +
577
+ `CROSS JOIN LATERAL (SELECT ${g.template} AS mason_x) AS mason_s ORDER BY mason_n`;
578
+ const got = pgEvalSync(sql, cols.map((_, i) => tuples.map((t) => t[i])), g.zone);
579
+ got.forEach(([v], i) => answers.set(`${key}\u0000${JSON.stringify(tuples[i])}`, v ?? null));
580
+ }
581
+ rows.forEach(([zone = null, template = null, ...args], r) => {
582
+ const v = zone === null || template === null || args.some((a) => a === null) ? null : answers.get(`${zone}\u0000${template}\u0000${JSON.stringify(args)}`) ?? null;
583
+ out.setItem(r, v === null ? null : k.read(v));
584
+ });
585
+ out.flush();
586
+ }
587
+ catch (e) {
588
+ // Postgres's SQLSTATE and words (src/duck/store.ts postgresWording reads them back)
589
+ info.setError(e instanceof PgEvalError ? `mason[${e.code}]: ${e.message}` : `mason: ${e.message}`);
590
+ }
591
+ },
592
+ }));
593
+ }
594
+ const steps = (json, either) => JSON.parse(json)
595
+ .map((s) => typeof s === "number" ? { index: s } : either ? { either: s } : { key: s });
596
+ /** The libc order's two functions (src/duck/libc-order.ts), for the order the source was read with: the characters
597
+ * ICU's accent and case levels read, and the key (a value holding one not covered refuses, by name). */
598
+ export function registerLibcOrder(conn, order) {
599
+ scalar(conn, "__mason_libc_kept", [VARCHAR], VARCHAR, ([s]) => libcKept(order, String(s)));
600
+ scalar(conn, "__mason_libc_key", [VARCHAR, VARCHAR], VARCHAR, ([s, icu]) => libcKey(order, String(s), String(icu)));
601
+ }
602
+ /**
603
+ * EVERY CALL CARRIES ITS SESSION, FIRST (the test server: a client leaving mid-statement hung the process
604
+ * for good). A scalar function runs on the thread that registered it — the executor worker (src/duck/executor.ts) —
605
+ * while an executor thread waits for it, so that thread can never wait for the executor — and closing a result or a
606
+ * connection does, to cancel the executor's tasks. An interrupt does not end those tasks (they run on until the close),
607
+ * so a statement that calls one of these functions is cancelled another way: its session's cell of the CANCEL TABLE
608
+ * (src/duck/cancel.ts) says it is cancelled, and the statement's next call raises. DuckDB may hand that error back
609
+ * while another task of the query still waits for a call, so the result is closed only once the session's calls have
610
+ * stopped (src/duck/store.ts `cleanUp`; a deadlock under load). The table is shared memory: the listener's thread writes it
611
+ * while this one is busy with the statement's calls. The rewrite (src/duck/rewrite.ts `fn`) passes
612
+ * `getvariable('__mason_session')` as every `__mason_*` function's first argument, which DuckDB folds to the session's
613
+ * literal at bind time; the session runner sets the variable on its connection (its slot, `s<n>`). A connection without
614
+ * it (the writer, a copy's) passes NULL: never cancelled this way.
615
+ */
616
+ export const SESSION_VARIABLE = "__mason_session";
617
+ export const CANCELLED = "mason[57014]: canceling statement due to user request";
618
+ /** how many vectors each function was called with (a call is a hop to the main thread): what a test or a measurement
619
+ * reads to know whether a statement reached a function at all (the rewrite answers a `->>` natively where it can) */
620
+ export const FUNCTION_CALLS = new Map();
621
+ /** the cancel table the calls read: the executor's, shared with the listener (`useCancelTable`), else one of this thread's own */
622
+ let cancels = cancelTable();
623
+ export function useCancelTable(t) { cancels = t; }
624
+ export function cancelsTable() { return cancels; }
625
+ /** when each session's last call was answered (performance.now(), on this thread): what a session's runner reads to know
626
+ * that no task of a statement still waits for a call before it closes the statement's result (src/duck/store.ts
627
+ * `cleanUp`) */
628
+ const lastCallAt = new Float64Array(CANCEL_SLOTS);
629
+ export function msSinceCall(slot) { return performance.now() - (lastCallAt[slot] ?? 0); }
630
+ /** the session's vector (the first argument, a constant): raises when the session's statement is cancelled */
631
+ function cancelled(info, vectors, rows) {
632
+ const slot = rows > 0 ? slotOf(text(vectors[0]?.getItem(0))) : null;
633
+ if (slot !== null)
634
+ lastCallAt[slot] = performance.now();
635
+ if (slot === null || !isCancelled(cancels, slot))
636
+ return false;
637
+ info.setError(CANCELLED);
638
+ return true;
639
+ }
640
+ /** Registers the scalar functions on the instance (once: every connection of it sees them). */
641
+ export function registerFunctions(conn) {
642
+ for (const kind of Object.keys(PG_KINDS))
643
+ viaPostgres(conn, kind);
644
+ scalar(conn, "__mason_case", [VARCHAR, BOOLEAN, VARCHAR], VARCHAR, ([s, up, mode]) => pgCase(String(s), up === true, String(mode)));
645
+ scalar(conn, "__mason_initcap", [VARCHAR, VARCHAR], VARCHAR, ([s, mode]) => initcap(String(s), String(mode)));
646
+ // (json, steps as a JSON array, a #> path's, jsonb's canonical text, ->>'s text)
647
+ scalar(conn, "__mason_json_get", [VARCHAR, VARCHAR, BOOLEAN, BOOLEAN, BOOLEAN], VARCHAR, ([j, s, either, canonical, asText]) => jsonGet(String(j), steps(String(s), either === true), canonical === true, asText === true));
648
+ // the same, as a stored JSON path keeps it (keptJsonValue, src/duck/kept.ts): what the copy's writes compute
649
+ scalar(conn, "__mason_json_kept", [VARCHAR, VARCHAR, BOOLEAN, BOOLEAN, BOOLEAN], VARCHAR, ([j, s, either, canonical, asText]) => keptJsonValue(String(j), steps(String(s), either === true), canonical === true, asText === true));
650
+ scalar(conn, "__mason_json_elements", [VARCHAR, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, asText]) => listValue(jsonElements(String(j), canonical === true, asText === true)));
651
+ scalar(conn, "__mason_json_keys", [VARCHAR, BOOLEAN], LIST(VARCHAR), ([j, canonical]) => listValue(jsonMembers(String(j), canonical === true, false).map(([k]) => k)));
652
+ scalar(conn, "__mason_json_values", [VARCHAR, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, asText]) => listValue(jsonMembers(String(j), canonical === true, asText === true).map(([, v]) => v)));
653
+ scalar(conn, "__mason_jsonb_text", [VARCHAR], VARCHAR, ([j]) => jsonbText(String(j)));
654
+ // jsonb's @>, ?, ?| and ?& (DuckDB's json_contains and json_exists answer otherwise on arrays), json(b)_array_length
655
+ scalar(conn, "__mason_jsonb_contains", [VARCHAR, VARCHAR], BOOLEAN, ([a, b]) => jsonbContains(String(a), String(b)));
656
+ scalar(conn, "__mason_jsonb_exists", [VARCHAR, VARCHAR], BOOLEAN, ([j, k]) => jsonbExists(String(j), String(k)));
657
+ // ?| and ?&: a NULL key is skipped, as jsonb_exists_any / _all skip it
658
+ scalar(conn, "__mason_jsonb_exists_any", [VARCHAR, LIST(VARCHAR)], BOOLEAN, ([j, ks]) => itemsOf(ks).some((k) => k !== null && jsonbExists(String(j), k)));
659
+ scalar(conn, "__mason_jsonb_exists_all", [VARCHAR, LIST(VARCHAR)], BOOLEAN, ([j, ks]) => itemsOf(ks).every((k) => k === null || jsonbExists(String(j), k)));
660
+ scalar(conn, "__mason_json_array_length", [VARCHAR], INTEGER, ([j]) => jsonArrayLength(String(j)));
661
+ scalar(conn, "__mason_string_to_array", [VARCHAR, VARCHAR, VARCHAR], LIST(VARCHAR), ([s, d, n]) => s === null ? null : listValue(stringToArray(String(s), text(d), text(n))), { nulls: "pass" });
662
+ scalar(conn, "__mason_json_value", [VARCHAR, VARCHAR], VARCHAR, ([kind, v]) => jsonValue(String(kind), text(v)), { nulls: "pass" });
663
+ scalar(conn, "__mason_json_array", [VARCHAR, LIST(VARCHAR)], VARCHAR, ([kind, a]) => jsonArray(String(kind), itemsOf(a)));
664
+ scalar(conn, "__mason_json_build", [], VARCHAR, ([shape, ...args]) => jsonBuild(String(shape), args.map(text)), { nulls: "pass", varargs: true });
665
+ // to_char, Postgres's template patterns (src/duck/to-char.ts): a time by its text in the session's zone, a number by its digits
666
+ scalar(conn, "__mason_to_char_time", [VARCHAR, VARCHAR], VARCHAR, ([v, f]) => toCharTime(String(v), String(f)));
667
+ scalar(conn, "__mason_to_char_number", [VARCHAR, VARCHAR], VARCHAR, ([v, f]) => toCharNumber(String(v), String(f)));
668
+ // a value's text as Postgres's output function writes it, where DuckDB's cast to VARCHAR writes another: a float's
669
+ // shortest digits (`0`, `NaN`, `Infinity`, `-0`; DuckDB's `0.0`, `nan`, `inf`), an interval's `1 mon` (DuckDB's `1 month`)
670
+ scalar(conn, "__mason_float_text", [DOUBLE, BOOLEAN], VARCHAR, ([x, four]) => four === true ? formatFloat(Math.fround(Number(x)), "float4") : formatFloat(Number(x), "float8"));
671
+ // a zone read from a row (AT TIME ZONE z, src/duck/rewrite.ts zoneArg): an IANA name, spelled as tzdata spells it;
672
+ // an abbreviation or a POSIX zone, which Postgres reads with a fixed offset and DuckDB as another zone, refused
673
+ scalar(conn, "__mason_zone", [VARCHAR], VARCHAR, ([z]) => {
674
+ const read = atTimeZoneOf(String(z));
675
+ if ("problem" in read)
676
+ throw new PgEvalError(read.sqlstate, read.sqlstate === "0A000" ? `${read.problem} (${gapMark("zone-abbreviation")})` : read.problem);
677
+ return read.zone;
678
+ });
679
+ // a division's divisor tested for zero (src/duck/rewrite.ts nonZero): Postgres's error, from a function DuckDB takes
680
+ // as deterministic, where error() is volatile and refused in a DISTINCT aggregate's ORDER BY
681
+ scalar(conn, "__mason_zero_divisor", [BOOLEAN], BOOLEAN, ([zero]) => { if (zero === true)
682
+ throw new PgEvalError("22012", "division by zero"); return false; });
683
+ // a numeric divided by a numeric (src/duck/rewrite.ts, `/`): Postgres's quotient, rounded at its scale (numeric_div),
684
+ // as the nearest double, so two quotients Postgres finds equal (0.3 / 0.1 and 0.6 / 0.2) compare, group and count as
685
+ // one, where DuckDB's double division gives each its own last bits. The text a client reads is the exact program's
686
+ scalar(conn, "__mason_numeric_div", [VARCHAR, VARCHAR], DOUBLE, ([a, b]) => {
687
+ const x = String(a), y = String(b);
688
+ // an unconstrained numeric held as a double may be NaN or ±Infinity (DuckDB writes `nan`, `inf`): Postgres's
689
+ // special values divide as a double's do, except by zero
690
+ if (!/^[+-]?[\d.]/.test(x) || !/^[+-]?[\d.]/.test(y)) {
691
+ if (Number(y) === 0 && !/nan/i.test(x))
692
+ throw new PgEvalError("22012", "division by zero");
693
+ // a finite value over ±Infinity is 0, never a double's -0
694
+ const q = Number(x.replace(/inf/i, "Infinity")) / Number(y.replace(/inf/i, "Infinity"));
695
+ return q === 0 ? 0 : q;
696
+ }
697
+ try {
698
+ return Number(numericText(div(parseNumeric(x), parseNumeric(y))));
699
+ }
700
+ catch (e) {
701
+ if (e instanceof NumericError)
702
+ throw new PgEvalError(e.sqlstate, e.message);
703
+ throw e;
704
+ }
705
+ });
706
+ // a numeric Postgres's own function computed, summed (src/duck/rewrite.ts summed): numeric_out's text as two parts
707
+ // DuckDB's DECIMAL(38,18) holds exactly — its integer digits and first 18 decimals (part 0), and its next 18 decimals
708
+ // as the decimals of a value below 1 (part 1), each with the sign — refused, by name, past 20 integer digits or 36
709
+ // decimals. NaN and ±Infinity are none (NULL): a sum holding one is the double's, which adds them as Postgres does
710
+ // (__mason_numeric_special)
711
+ scalar(conn, "__mason_numeric_exact", [VARCHAR, INTEGER], VARCHAR, ([t, part]) => {
712
+ const s = String(t);
713
+ if (!/^-?\d/.test(s))
714
+ return null;
715
+ const sign = s.startsWith("-") ? "-" : "";
716
+ const [whole, frac = ""] = s.replace(/^-/, "").split(".");
717
+ const kept = frac.replace(/0+$/, "");
718
+ if (whole.replace(/^0+(?=\d)/, "").length > 20 || kept.length > 36)
719
+ throw new PgEvalError("0A000", `the sum of the numeric ${s} is not supported by Mason (${gapMark("numeric-exact-sum")})`);
720
+ return part === 0 ? `${sign}${whole}.${kept.slice(0, 18) || "0"}` : `${sign}0.${kept.slice(18) || "0"}`;
721
+ });
722
+ // a text read as an enum (src/duck/rewrite.ts enumValue): the label, or Postgres's error for one the type lacks
723
+ scalar(conn, "__mason_enum_in", [VARCHAR, VARCHAR, LIST(VARCHAR)], VARCHAR, ([t, name, labels]) => {
724
+ const label = String(t);
725
+ if (!itemsOf(labels).includes(label))
726
+ throw new PgEvalError("22P02", `invalid input value for enum ${String(name)}: "${label}"`);
727
+ return label;
728
+ });
729
+ scalar(conn, "__mason_numeric_special", [VARCHAR], BOOLEAN, ([t]) => !/^-?\d/.test(String(t)));
730
+ // a numeric program over a row's cells' texts (src/duck/rewrite.ts exactText): Postgres's exact value as numeric_out
731
+ // writes it, where DuckDB computes a double (a quotient handed to ln)
732
+ scalar(conn, "__mason_numeric_program", [], VARCHAR, ([program, ...cells]) => {
733
+ const values = Object.fromEntries(cells.map((c, i) => [String(i), text(c)]));
734
+ try {
735
+ return evaluateText(JSON.parse(String(program)), values);
736
+ }
737
+ catch (e) {
738
+ if (e instanceof NumericError)
739
+ throw new PgEvalError(e.sqlstate, e.message);
740
+ throw e;
741
+ }
742
+ }, { nulls: "pass", varargs: true });
743
+ // the decimals of numeric_out's text: the value's display scale (`2.450` has 3)
744
+ scalar(conn, "__mason_numeric_scale", [VARCHAR], INTEGER, ([t]) => { const at = String(t).indexOf("."); return at < 0 ? 0 : String(t).length - at - 1; });
745
+ scalar(conn, "__mason_float_numeric_text", [DOUBLE, BOOLEAN], VARCHAR, ([x, four]) => floatNumericText(Number(x), four === true));
746
+ scalar(conn, "__mason_interval_text", [INTERVAL], VARCHAR, ([v]) => { const i = v; return intervalText(i.months, i.days, i.micros); });
747
+ scalar(conn, "__mason_interval_part", [VARCHAR, INTERVAL], VARCHAR, ([f, v]) => { const i = v; return intervalPart(String(f), i.months, i.days, i.micros); });
748
+ scalar(conn, "__mason_justify", [VARCHAR, INTERVAL], INTERVAL, ([k, v]) => { const i = v; const [m, d, t] = justify(String(k), i.months, i.days, i.micros); return new DuckDBIntervalValue(m, d, t); });
749
+ // jsonb's `-`, `||` and jsonb_strip_nulls (canonical text), quote_literal, array_to_string's NULLs
750
+ scalar(conn, "__mason_jsonb_delete", [VARCHAR, VARCHAR], VARCHAR, ([j, k]) => jsonbDelete(String(j), String(k)));
751
+ scalar(conn, "__mason_jsonb_concat", [VARCHAR, VARCHAR], VARCHAR, ([a, b]) => jsonbConcat(String(a), String(b)));
752
+ scalar(conn, "__mason_jsonb_strip_nulls", [VARCHAR], VARCHAR, ([j]) => jsonbStripNulls(String(j)));
753
+ scalar(conn, "__mason_quote_literal", [VARCHAR], VARCHAR, ([t]) => quoteLiteral(String(t)));
754
+ // a bytea's encode / decode (its bytes as their hex, either way: DuckDB's hex and unhex carry them)
755
+ scalar(conn, "__mason_bytea_encode", [VARCHAR, VARCHAR], VARCHAR, ([h, f]) => byteaEncode(Buffer.from(String(h), "hex"), String(f)));
756
+ scalar(conn, "__mason_bytea_decode", [VARCHAR, VARCHAR], VARCHAR, ([t, f]) => byteaDecode(String(t), String(f)).toString("hex"));
757
+ // a row's text as record_out writes it (each column its own text already)
758
+ scalar(conn, "__mason_record_text", [], VARCHAR, (cols) => recordText(cols.map(text)), { nulls: "pass", varargs: true });
759
+ scalar(conn, "__mason_array_to_string", [LIST(VARCHAR), VARCHAR, VARCHAR], VARCHAR, ([a, sep, n]) => a === null || sep === null ? null : arrayToString(itemsOf(a), String(sep), text(n)), { nulls: "pass" });
760
+ // arrays, as their elements' text (the rewrite casts an array to VARCHAR[] and the answer back to its type)
761
+ scalar(conn, "__mason_array_text", [LIST(VARCHAR), BOOLEAN], VARCHAR, ([a, bool]) => arrayText(itemsOf(a), bool === true));
762
+ // array_remove(a, v): every element not v; a NULL v removes the NULLs
763
+ scalar(conn, "__mason_array_remove", [LIST(VARCHAR), VARCHAR], LIST(VARCHAR), ([a, v]) => a === null ? null : listValue(itemsOf(a).filter((x) => v === null ? x !== null : x !== String(v))), { nulls: "pass" });
764
+ }