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