@mason-data/cli 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +15 -13
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
|
@@ -10,51 +10,206 @@
|
|
|
10
10
|
* binds a lambda MACRO's constant arguments wrongly once its postgres extension is loaded: no macro is used).
|
|
11
11
|
* Every name is `__mason_*`, which no Postgres function has.
|
|
12
12
|
*/
|
|
13
|
-
import
|
|
13
|
+
import duckdb from "@duckdb/node-bindings";
|
|
14
|
+
import { ANY, BIGINT, BLOB, BOOLEAN, DATE, DOUBLE, DuckDBBlobValue, DuckDBDataChunk, DuckDBDateValue, DuckDBIntervalValue, DuckDBScalarFunction, DuckDBTimestampTZValue, DuckDBTimestampValue, INTEGER, INTERVAL, LIST, TIMESTAMP, TIMESTAMPTZ, VARCHAR, listValue } from "@duckdb/node-api";
|
|
14
15
|
import { pgEvalSync, PgEvalError } from "./pg-eval.js";
|
|
15
16
|
import { MARKED_FUNCTIONS } from "./kept-route.js";
|
|
16
|
-
import {
|
|
17
|
+
import { libcKey } from "./libc-order.js";
|
|
17
18
|
import { formatFloat } from "../api/postgres/encode.js";
|
|
18
19
|
import { floatAsNumeric } from "./types.js";
|
|
19
20
|
import { intervalText } from "./types.js";
|
|
20
21
|
import { jsonbText, parseJson, writeJson, quoteJson } from "./jsonb.js";
|
|
21
|
-
import { JSONB_KEY_FUNCTIONS, jsonbKey,
|
|
22
|
-
import { compare, div, evaluateText, NumericError, numericText, parseNumeric } from "../lib/pg-numeric.js";
|
|
22
|
+
import { JSONB_KEY_FUNCTIONS, jsonbKey, jsonbStrings, STORED_SUFFIX } from "./jsonb-order.js";
|
|
23
|
+
import { compare, div, evaluateText, fitsEngineType, NumericError, numericOrderKey, numericText, numericToDouble, numericToInteger, parseNumeric, parseProgram } from "../lib/pg-numeric.js";
|
|
23
24
|
import { toCharNumber, toCharTime } from "./to-char.js";
|
|
24
25
|
import { atTimeZoneOf } from "../lib/timezone.js";
|
|
25
26
|
import { gapMark } from "./gaps.js";
|
|
26
|
-
import {
|
|
27
|
+
import { beat, cancelTable, isCancelled, slotOf } from "./cancel.js";
|
|
27
28
|
// ------------------------------------------------------------------------------------------------- case mapping
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
//
|
|
30
|
+
// upper(), lower() and initcap() as Postgres computes them under a collation's case mapping (issue 406). WHICH mapping
|
|
31
|
+
// a call uses is src/duck/collation.ts's `caseOf` (the call's input collation, Postgres's analysis says which); this
|
|
32
|
+
// is only what each one does. Every detail was measured on Postgres 18 (glibc 2.41, ICU 76: Unicode 16) over every
|
|
33
|
+
// code point of the BMP and the SMP but the CJK and Hangul blocks, and held there by tests/duck-functions.test.ts.
|
|
34
|
+
/** Letters this runtime's tables map that the source's do not: added in Unicode 17, after Postgres 18's own tables,
|
|
35
|
+
* glibc's and ICU's (Unicode 16). Kept as they are, as Postgres keeps them. */
|
|
36
|
+
const NEWER = /[-ꟕ\u{16EA0}-\u{16EB8}\u{16EBB}-\u{16ED3}]/u;
|
|
37
|
+
const NEWER_RUNS = /([-ꟕ\u{16EA0}-\u{16EB8}\u{16EBB}-\u{16ED3}]+)/u;
|
|
38
|
+
/** Unicode's simple uppercase where its full one is several characters and the simple one is not the letter itself:
|
|
39
|
+
* the Greek letters with a iota subscript (`ᾳ` is `ᾼ`, where the full mapping is `ΑΙ`) */
|
|
40
|
+
const SIMPLE_UPPER = new Map([
|
|
41
|
+
...[0x1f80, 0x1f90, 0x1fa0].flatMap((b) => Array.from({ length: 8 }, (_, i) => [String.fromCodePoint(b + i), String.fromCodePoint(b + 8 + i)])),
|
|
42
|
+
["ᾳ", "ᾼ"], ["ῃ", "ῌ"], ["ῳ", "ῼ"],
|
|
43
|
+
]);
|
|
44
|
+
/** Unicode's titlecase where it is not the uppercase (UnicodeData's titlecase field and SpecialCasing's): the digraphs
|
|
45
|
+
* (`dž` is `Dž`), the ligatures (`fi` is `Fi`, `ß` is `Ss`), the Greek letters with a iota subscript, and the Georgian
|
|
46
|
+
* Mkhedruli letters, whose titlecase is themselves (their uppercase is Mtavruli) */
|
|
47
|
+
const TITLE = new Map([
|
|
48
|
+
["ß", "Ss"], ["և", "Եւ"], ["ff", "Ff"], ["fi", "Fi"], ["fl", "Fl"], ["ffi", "Ffi"], ["ffl", "Ffl"], ["ſt", "St"], ["st", "St"],
|
|
49
|
+
["ﬓ", "Մն"], ["ﬔ", "Մե"], ["ﬕ", "Մի"], ["ﬖ", "Վն"], ["ﬗ", "Մխ"],
|
|
50
|
+
...[[0x01c4, 0x01c5], [0x01c5, 0x01c5], [0x01c6, 0x01c5], [0x01c7, 0x01c8], [0x01c8, 0x01c8], [0x01c9, 0x01c8], [0x01ca, 0x01cb], [0x01cb, 0x01cb], [0x01cc, 0x01cb],
|
|
51
|
+
[0x01f1, 0x01f2], [0x01f2, 0x01f2], [0x01f3, 0x01f2]].map(([a, b]) => [String.fromCodePoint(a), String.fromCodePoint(b)]),
|
|
52
|
+
...[...SIMPLE_UPPER].flatMap(([lower, title]) => [[lower, title], [title, title]]),
|
|
53
|
+
["ᾲ", "Ὰͅ"], ["ᾴ", "Άͅ"], ["ᾷ", "ᾼ͂"], ["ῂ", "Ὴͅ"], ["ῄ", "Ήͅ"],
|
|
54
|
+
["ῇ", "ῌ͂"], ["ῲ", "Ὼͅ"], ["ῴ", "Ώͅ"], ["ῷ", "ῼ͂"],
|
|
55
|
+
...[...Array.from({ length: 0x2b }, (_, i) => 0x10d0 + i), 0x10fd, 0x10fe, 0x10ff].map((c) => [String.fromCodePoint(c), String.fromCodePoint(c)]),
|
|
56
|
+
]);
|
|
57
|
+
/** one character's simple mapping (towupper / towlower, Postgres's builtin C.UTF-8): its full mapping where that is one
|
|
58
|
+
* character, else Unicode's simple one where it has one (`İ` lowers to `i`), else itself (`ß`, `ʼn`) */
|
|
30
59
|
function simple(ch, upper) {
|
|
60
|
+
if (NEWER.test(ch))
|
|
61
|
+
return ch;
|
|
62
|
+
if (upper) {
|
|
63
|
+
const s = SIMPLE_UPPER.get(ch);
|
|
64
|
+
if (s !== undefined)
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
else if (ch === "İ")
|
|
68
|
+
return "i";
|
|
31
69
|
const m = upper ? ch.toUpperCase() : ch.toLowerCase();
|
|
32
70
|
return [...m].length === 1 ? m : ch;
|
|
33
71
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
40
|
-
|
|
72
|
+
/** a whole string's full mapping, under an ICU language (`und` for the root's), the letters NEWER kept. ICU's Armenian
|
|
73
|
+
* uppercases the ligature `և` to `ԵՎ`, which the runtime's leaves to the root's `ԵՒ` */
|
|
74
|
+
function full(s, upper, language) {
|
|
75
|
+
const map = (x) => upper ? (language === "hy" ? x.replaceAll("և", "ԵՎ") : x).toLocaleUpperCase(language) : x.toLocaleLowerCase(language);
|
|
76
|
+
if (!NEWER.test(s))
|
|
77
|
+
return map(s);
|
|
78
|
+
return s.split(NEWER_RUNS).map((x, i) => i % 2 === 1 ? x : map(x)).join("");
|
|
79
|
+
}
|
|
80
|
+
/** one character's full titlecase under an ICU language (`tr`'s `i` is `İ`, Armenian's `և` is `Եվ`); ICU's Greek
|
|
81
|
+
* uppercasing (accents dropped) is its uppercase's alone */
|
|
82
|
+
function title(ch, language) {
|
|
83
|
+
if (NEWER.test(ch))
|
|
84
|
+
return ch;
|
|
85
|
+
if (language === "hy" && ch === "և")
|
|
86
|
+
return "Եվ";
|
|
87
|
+
return TITLE.get(ch) ?? ch.toLocaleUpperCase(language === "el" ? "und" : language);
|
|
88
|
+
}
|
|
89
|
+
/** what a word is made of, for initcap's per character mappings: a letter or a digit as each locale's ctype says */
|
|
90
|
+
const ALNUM = {
|
|
91
|
+
ascii: /^[A-Za-z0-9]$/,
|
|
92
|
+
libc: /^[\p{Alphabetic}\p{Nd}]$/u,
|
|
93
|
+
simple: /^[\p{Alphabetic}0-9]$/u,
|
|
94
|
+
full: /^[\p{Alphabetic}\p{Nd}]$/u,
|
|
95
|
+
};
|
|
96
|
+
/** upper() / lower() as Postgres computes them under that case mapping (libc's upper is the rewrite's SQL, not this) */
|
|
97
|
+
export function pgCase(s, upper, mapping) {
|
|
98
|
+
if (mapping === "ascii")
|
|
41
99
|
return s.replace(upper ? /[a-z]+/g : /[A-Z]+/g, (x) => upper ? x.toUpperCase() : x.toLowerCase());
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
if (mapping === "libc" || mapping === "simple") {
|
|
101
|
+
let out = "";
|
|
102
|
+
for (const ch of s)
|
|
103
|
+
out += simple(ch, upper);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
if (mapping === "full")
|
|
107
|
+
return full(s, upper, "und");
|
|
108
|
+
return full(s, upper, icuOf(mapping).language);
|
|
109
|
+
}
|
|
110
|
+
/** an ICU mapping's tag (src/duck/collation.ts icuLocale) and the language its casing reads */
|
|
111
|
+
function icuOf(mapping) {
|
|
112
|
+
if (!mapping.startsWith("icu:"))
|
|
113
|
+
throw new Error(`the case mapping ${JSON.stringify(mapping)} is not one Mason knows (Mason bug: report it)`);
|
|
114
|
+
const tag = mapping.slice(4);
|
|
115
|
+
return { tag, language: tag.split("-")[0].toLowerCase() };
|
|
46
116
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
117
|
+
/**
|
|
118
|
+
* initcap() as Postgres computes it under that case mapping:
|
|
119
|
+
* - libc (glibc's towupper / towlower), C's ASCII letters, and the builtin C.UTF-8: per character, a letter or digit
|
|
120
|
+
* after a letter or digit lowered, every other raised (str_initcap; C.UTF-8 raises to the uppercase, never the
|
|
121
|
+
* titlecase: `dž` is `DŽ`);
|
|
122
|
+
* - the builtin PG_UNICODE_FAST: a word is a run of letters and digits, its first character in its full TITLECASE
|
|
123
|
+
* (`dž` is `Dž`, `ß` is `Ss`), the rest lowered;
|
|
124
|
+
* - ICU: u_strToTitle's, a word as ICU's word break says (`o'hara`, `hello.world` and `a_b` are one word each), its
|
|
125
|
+
* first letter, digit, symbol or private use character in its titlecase under the locale's language (Dutch's `ij` is
|
|
126
|
+
* `IJ`), the characters before it kept, the rest lowered.
|
|
127
|
+
*/
|
|
128
|
+
export function initcap(s, mapping) {
|
|
129
|
+
if (mapping.startsWith("icu:"))
|
|
130
|
+
return icuTitle(s, icuOf(mapping));
|
|
131
|
+
let out = "";
|
|
132
|
+
if (mapping === "full") {
|
|
133
|
+
// a run of word characters: its first titlecased, the rest lowered in the string's context (a final sigma's)
|
|
134
|
+
let end = 0;
|
|
135
|
+
for (const run of s.match(/[\p{Alphabetic}\p{Nd}]+|[^\p{Alphabetic}\p{Nd}]+/gu) ?? []) {
|
|
136
|
+
end += run.length;
|
|
137
|
+
const first = String.fromCodePoint(run.codePointAt(0));
|
|
138
|
+
out += title(first, "und") + lowerRest(first, run.slice(first.length), s.slice(end), "und");
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
const word = ALNUM[mapping];
|
|
143
|
+
let was = false;
|
|
50
144
|
for (const ch of s) {
|
|
51
|
-
if (
|
|
145
|
+
if (mapping === "ascii")
|
|
52
146
|
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
147
|
else
|
|
56
148
|
out += simple(ch, !was);
|
|
57
|
-
was =
|
|
149
|
+
was = word.test(ch);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
/** the rest of a word lowered in the string's context: after its titlecased first character (`first`, as the source
|
|
154
|
+
* had it) and before `after`, the string's text past the word. The context Unicode's conditional mappings read: a
|
|
155
|
+
* final sigma (a cased letter before it, none after its case-ignorable characters), and Turkish's and Azeri's dot
|
|
156
|
+
* above after a capital I (dropped, as `I` + U+0307 lowers to `i`) */
|
|
157
|
+
function lowerRest(first, rest, after, language) {
|
|
158
|
+
if (rest === "")
|
|
159
|
+
return "";
|
|
160
|
+
let r = rest;
|
|
161
|
+
if ((language === "tr" || language === "az") && first === "I" && r.startsWith("̇"))
|
|
162
|
+
r = r.slice(1);
|
|
163
|
+
// (a cased letter that lowers to itself stands for the first character, which is before every one of the rest)
|
|
164
|
+
const lead = /^\p{Cased}$/u.test(first) ? "a" : "";
|
|
165
|
+
let out = full(lead + r, false, language).slice(lead.length);
|
|
166
|
+
if (out.endsWith("ς") && r.endsWith("Σ") && /^\p{Case_Ignorable}*\p{Cased}/u.test(after))
|
|
167
|
+
out = `${out.slice(0, -1)}σ`;
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
/** a character u_strToTitle titlecases a word at: a letter (a modifier letter only where it is cased), a number, a
|
|
171
|
+
* symbol or a private use character (ustrcase_isLNS) */
|
|
172
|
+
const LNS = /^(?:[\p{Lu}\p{Ll}\p{Lt}\p{Lo}\p{N}\p{S}\p{Co}]|(?=\p{Lm})\p{Cased})$/u;
|
|
173
|
+
/** where the source's ICU (76) breaks a word and this runtime's (78) does not: a colon (CLDR's root tailoring leaves
|
|
174
|
+
* it out of MidLetter; Swedish's and Finnish's keep it in) and a cedilla. Split again, which changes nothing where the
|
|
175
|
+
* runtime breaks there itself */
|
|
176
|
+
const ICU_WORD_BREAK = /([:﹕:¸])/, ICU_WORD_BREAK_SV = /([¸])/;
|
|
177
|
+
const segmenters = new Map();
|
|
178
|
+
/** ICU's initcap: u_strToTitle with its word break iterator of the locale, its casing under the locale's language */
|
|
179
|
+
function icuTitle(s, { tag, language }) {
|
|
180
|
+
let seg = segmenters.get(tag);
|
|
181
|
+
if (seg === undefined) {
|
|
182
|
+
seg = new Intl.Segmenter(tag, { granularity: "word" });
|
|
183
|
+
segmenters.set(tag, seg);
|
|
184
|
+
}
|
|
185
|
+
const breaks = language === "sv" || language === "fi" ? ICU_WORD_BREAK_SV : ICU_WORD_BREAK;
|
|
186
|
+
let out = "", end = 0;
|
|
187
|
+
for (const { segment } of seg.segment(s)) {
|
|
188
|
+
for (const word of segment.split(breaks)) {
|
|
189
|
+
if (word === "")
|
|
190
|
+
continue;
|
|
191
|
+
end += word.length;
|
|
192
|
+
const chars = [...word];
|
|
193
|
+
const at = chars.findIndex((c) => LNS.test(c));
|
|
194
|
+
if (at < 0) {
|
|
195
|
+
out += word;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const first = chars[at];
|
|
199
|
+
let t = title(first, language), next = at + 1;
|
|
200
|
+
// Dutch: a word's `ij` is `IJ`, its `íj́` `ÍJ́` (ICU's maybeTitleDutchIJ)
|
|
201
|
+
if (language === "nl" && (chars[next] === "j" || chars[next] === "J")) {
|
|
202
|
+
if (t === "I" && chars[next + 1] !== "́") {
|
|
203
|
+
t += "J";
|
|
204
|
+
next++;
|
|
205
|
+
}
|
|
206
|
+
else if (t === "Í" && chars[next + 1] === "́") {
|
|
207
|
+
t += "J́";
|
|
208
|
+
next += 2;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
out += chars.slice(0, at).join("") + t + lowerRest(first, chars.slice(next).join(""), s.slice(end), language);
|
|
212
|
+
}
|
|
58
213
|
}
|
|
59
214
|
return out;
|
|
60
215
|
}
|
|
@@ -477,6 +632,29 @@ export function stringToArray(s, delimiter, nullString) {
|
|
|
477
632
|
}
|
|
478
633
|
const itemsOf = (v) => v.items.map((x) => x === null ? null : String(x));
|
|
479
634
|
// ------------------------------------------------------------------------------------------------- registration
|
|
635
|
+
/** a NumericError as the error a Mason function raises: Postgres's DETAIL after its message (`\nDETAIL: `), which
|
|
636
|
+
* src/duck/store.ts postgresWording sends apart, as Postgres does */
|
|
637
|
+
const numericEvalError = (e) => new PgEvalError(e.sqlstate, e.detail === null ? e.message : `${e.message}\nDETAIL: ${e.detail}`);
|
|
638
|
+
/** a `__mason_numeric_program` literal parsed once (`parseProgram`): the same text is given on every row of a statement,
|
|
639
|
+
* so the last few are kept by their text (a program is never changed once made, so one parse serves every row) */
|
|
640
|
+
const PARSED = new Map();
|
|
641
|
+
const PARSED_KEPT = 64;
|
|
642
|
+
function parsedProgram(text) {
|
|
643
|
+
const known = PARSED.get(text);
|
|
644
|
+
if (known !== undefined)
|
|
645
|
+
return known;
|
|
646
|
+
const p = parseProgram(text);
|
|
647
|
+
if (PARSED.size >= PARSED_KEPT)
|
|
648
|
+
PARSED.delete(PARSED.keys().next().value);
|
|
649
|
+
PARSED.set(text, p);
|
|
650
|
+
return p;
|
|
651
|
+
}
|
|
652
|
+
/** a numeric's EQUALITY KEY (src/duck/rewrite.ts exactKey): its exact text without trailing zeros, so two values
|
|
653
|
+
* Postgres finds equal (5.0 and 5.00, 0 and -0.0) are one key and two it tells apart are two, whatever their doubles */
|
|
654
|
+
function numericKey(s) {
|
|
655
|
+
const kept = /^-?\d/.test(s) && s.includes(".") ? s.replace(/0+$/, "").replace(/\.$/, "") : s;
|
|
656
|
+
return kept === "-0" ? "0" : kept;
|
|
657
|
+
}
|
|
480
658
|
const text = (v) => v === null || v === undefined ? null : String(v);
|
|
481
659
|
/** a scalar function over VARCHAR (and BOOLEAN) arguments, one call per row; a thrown error is the statement's */
|
|
482
660
|
function scalar(conn, name, params, returns, body, opts = {}) {
|
|
@@ -496,6 +674,7 @@ function scalar(conn, name, params, returns, body, opts = {}) {
|
|
|
496
674
|
const vectors = all.slice(1);
|
|
497
675
|
try {
|
|
498
676
|
for (let r = 0; r < input.rowCount; r++) {
|
|
677
|
+
beat(cancels); // one vector of wide documents is seconds of this loop: the watch must see it run
|
|
499
678
|
const args = vectors.map((v) => v.getItem(r));
|
|
500
679
|
out.setItem(r, opts.nulls !== "pass" && args.some((a) => a === null) ? null : body(args));
|
|
501
680
|
}
|
|
@@ -504,7 +683,83 @@ function scalar(conn, name, params, returns, body, opts = {}) {
|
|
|
504
683
|
catch (e) {
|
|
505
684
|
// Postgres's own message where the function raises one (src/duck/store.ts postgresWording reads the prefix), and
|
|
506
685
|
// its SQLSTATE where the function says it
|
|
507
|
-
info.setError(e
|
|
686
|
+
info.setError(raised(e));
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
}));
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* A PROGRAM'S EQUALITY KEY IN ONE CALL (src/duck/rewrite.ts keyedGroups, issue 665): `__mason_numeric_key` of
|
|
693
|
+
* `__mason_numeric_program`, computed in the one crossing a row. A GROUP BY's key is computed for every row of the
|
|
694
|
+
* table, so it is the one call of a grouped quotient that scales with the rows, and it is made cheap:
|
|
695
|
+
* - the program (the first argument after the session) is the rewrite's literal, the same on every row: read once a
|
|
696
|
+
* vector, never decoded again for each row (a program's text is a hundred characters), and parsed once (`parsedProgram`);
|
|
697
|
+
* - a program is a function of its cells alone (`evaluateText`: no state, no clock), so a row whose cells' texts another
|
|
698
|
+
* row of the program already had is given that row's key (`KEYS`, a few thousand kept a program): a week of the year
|
|
699
|
+
* over millions of rows computes its 366 days once. An error (22012) is raised by the row that computes it, as before.
|
|
700
|
+
* Where the program is not one literal (two texts in a vector: never written by the rewrite), each row is its own.
|
|
701
|
+
*/
|
|
702
|
+
const KEYS = new Map();
|
|
703
|
+
const KEYS_KEPT = 4096;
|
|
704
|
+
function programKeyFunction(conn) {
|
|
705
|
+
const name = "__mason_numeric_program_key";
|
|
706
|
+
conn.registerScalarFunction(DuckDBScalarFunction.create({
|
|
707
|
+
name, returnType: VARCHAR, varArgsType: VARCHAR, specialHandling: true,
|
|
708
|
+
mainFunction: (info, input, out) => {
|
|
709
|
+
FUNCTION_CALLS.set(name, (FUNCTION_CALLS.get(name) ?? 0) + 1);
|
|
710
|
+
FUNCTION_ROWS.set(name, (FUNCTION_ROWS.get(name) ?? 0) + input.rowCount);
|
|
711
|
+
const all = Array.from({ length: input.columnCount }, (_, c) => input.getColumnVector(c));
|
|
712
|
+
if (cancelled(info, all, input.rowCount))
|
|
713
|
+
return;
|
|
714
|
+
const [programs, ...cells] = all.slice(1);
|
|
715
|
+
const rows = input.rowCount, width = cells.length;
|
|
716
|
+
try {
|
|
717
|
+
const first = rows > 0 ? text(programs.getItem(0)) : null;
|
|
718
|
+
const constant = first !== null && text(programs.getItem(rows - 1)) === first;
|
|
719
|
+
let known = constant ? KEYS.get(first) : undefined;
|
|
720
|
+
if (constant && known === undefined) {
|
|
721
|
+
if (KEYS.size >= 64)
|
|
722
|
+
KEYS.delete(KEYS.keys().next().value);
|
|
723
|
+
known = new Map();
|
|
724
|
+
KEYS.set(first, known);
|
|
725
|
+
}
|
|
726
|
+
const parsed = constant ? parsedProgram(first) : null;
|
|
727
|
+
// the row's cells by their keys ("0", "1" …, an array's own), as the program reads them
|
|
728
|
+
const texts = new Array(width);
|
|
729
|
+
for (let r = 0; r < rows; r++) {
|
|
730
|
+
beat(cancels);
|
|
731
|
+
const program = constant ? parsed : (() => { const t = text(programs.getItem(r)); return t === null ? null : parsedProgram(t); })();
|
|
732
|
+
if (program === null) {
|
|
733
|
+
out.setItem(r, null);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
for (let i = 0; i < width; i++)
|
|
737
|
+
texts[i] = text(cells[i].getItem(r));
|
|
738
|
+
// (the cells' texts as one string: a NULL, which no numeric's text is, apart from every text)
|
|
739
|
+
const id = width === 1 ? (texts[0] ?? "\u0000") : texts.map((t) => t ?? "\u0000").join("\u0001");
|
|
740
|
+
let key = known?.get(id);
|
|
741
|
+
if (key === undefined) {
|
|
742
|
+
try {
|
|
743
|
+
const t = evaluateText(program, texts);
|
|
744
|
+
key = t === null ? null : numericKey(t);
|
|
745
|
+
}
|
|
746
|
+
catch (e) {
|
|
747
|
+
if (e instanceof NumericError)
|
|
748
|
+
throw numericEvalError(e);
|
|
749
|
+
throw e;
|
|
750
|
+
}
|
|
751
|
+
if (known !== undefined) {
|
|
752
|
+
if (known.size >= KEYS_KEPT)
|
|
753
|
+
known.clear();
|
|
754
|
+
known.set(id, key);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
out.setItem(r, key);
|
|
758
|
+
}
|
|
759
|
+
out.flush();
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
info.setError(raised(e));
|
|
508
763
|
}
|
|
509
764
|
},
|
|
510
765
|
}));
|
|
@@ -536,7 +791,7 @@ export const PG_KINDS = {
|
|
|
536
791
|
read: (t) => {
|
|
537
792
|
const parts = t.split(" ");
|
|
538
793
|
if (parts.length !== 3)
|
|
539
|
-
throw new PgEvalError("0A000", `an infinite interval
|
|
794
|
+
throw new PgEvalError("0A000", `an infinite interval is not supported by Mason (${gapMark("infinite-interval")})`);
|
|
540
795
|
return new DuckDBIntervalValue(Number(parts[0]), Number(parts[1]), BigInt(parts[2]));
|
|
541
796
|
},
|
|
542
797
|
},
|
|
@@ -577,13 +832,14 @@ function viaPostgres(conn, kind) {
|
|
|
577
832
|
if (kind === "date" || kind === "timestamp" || kind === "timestamptz" || READS_TIME.test(g.template)) {
|
|
578
833
|
const relative = tuples.flat().find((a) => a !== null && RELATIVE.test(a));
|
|
579
834
|
if (relative !== undefined)
|
|
580
|
-
throw new PgEvalError("0A000", `
|
|
835
|
+
throw new PgEvalError("0A000", `a text read as a time holding a relative word (now, today, tomorrow, yesterday) is not supported by Mason: it means the transaction's start (${gapMark("relative-time-literal")})`);
|
|
581
836
|
}
|
|
582
837
|
const n = tuples[0].length;
|
|
583
838
|
const cols = Array.from({ length: n }, (_, i) => `a${i + 1}`);
|
|
584
839
|
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
840
|
`CROSS JOIN LATERAL (SELECT ${g.template} AS mason_x) AS mason_s ORDER BY mason_n`;
|
|
586
|
-
const
|
|
841
|
+
const [zone, dateStyle] = g.zone.split(PG_SESSION_SEPARATOR);
|
|
842
|
+
const got = pgEvalSync(sql, cols.map((_, i) => tuples.map((t) => t[i])), zone, dateStyle);
|
|
587
843
|
got.forEach(([v], i) => answers.set(`${key}\u0000${JSON.stringify(tuples[i])}`, v ?? null));
|
|
588
844
|
}
|
|
589
845
|
rows.forEach(([zone = null, template = null, ...args], r) => {
|
|
@@ -594,22 +850,36 @@ function viaPostgres(conn, kind) {
|
|
|
594
850
|
}
|
|
595
851
|
catch (e) {
|
|
596
852
|
// Postgres's SQLSTATE and words (src/duck/store.ts postgresWording reads them back)
|
|
597
|
-
info.setError(e
|
|
853
|
+
info.setError(raised(e));
|
|
598
854
|
}
|
|
599
855
|
},
|
|
600
856
|
}));
|
|
601
857
|
}
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
858
|
+
const stepsOf = (path, either) => path.map((s) => typeof s === "number" ? { index: s } : either ? { either: s } : { key: s });
|
|
859
|
+
const steps = (json, either) => stepsOf(JSON.parse(json), either);
|
|
860
|
+
/** the libc orders read, by collcollate (src/duck/collation.ts names one in each `__mason_libc_key` call) */
|
|
861
|
+
const LIBC_ORDERS = new Map();
|
|
862
|
+
/** the connections the key's function is registered on */
|
|
863
|
+
const LIBC_KEYED = new WeakSet();
|
|
864
|
+
/**
|
|
865
|
+
* A LIBC ORDER'S FUNCTIONS (src/duck/libc-order.ts), one order per collcollate: a text's key under the order the
|
|
866
|
+
* call names (`__mason_libc_key(s, collate)`; a value holding a character not covered refuses, by name), and — for the
|
|
867
|
+
* DATABASE's order alone (`database`) — jsonb's order under it (src/duck/jsonb-order.ts: Postgres compares a jsonb's
|
|
868
|
+
* strings under the database's collation, whatever the column's).
|
|
869
|
+
*/
|
|
870
|
+
export function registerLibcOrder(conn, collate, order, database) {
|
|
871
|
+
LIBC_ORDERS.set(collate, order);
|
|
872
|
+
if (!LIBC_KEYED.has(conn)) {
|
|
873
|
+
LIBC_KEYED.add(conn);
|
|
874
|
+
scalar(conn, "__mason_libc_key", [VARCHAR, VARCHAR], VARCHAR, ([s, c]) => {
|
|
875
|
+
const named = LIBC_ORDERS.get(String(c));
|
|
876
|
+
if (named === undefined)
|
|
877
|
+
throw new Error(`the libc collation ${String(c)}'s order was not read (Mason bug: report it)`);
|
|
878
|
+
return libcKey(named, String(s));
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (database)
|
|
882
|
+
keyScalar(conn, JSONB_KEY_FUNCTIONS.libc.key, [VARCHAR], BLOB, ([j]) => new DuckDBBlobValue(jsonbKey(String(j), { kind: "libc", order })));
|
|
613
883
|
}
|
|
614
884
|
/**
|
|
615
885
|
* A FUNCTION OF A JSONB'S KEY (src/duck/jsonb-order.ts `JSONB_KEY_FUNCTIONS`), and its stored twin (`STORED_SUFFIX`),
|
|
@@ -638,17 +908,29 @@ function icuKeysOf(v) {
|
|
|
638
908
|
* EVERY CALL CARRIES ITS SESSION, FIRST (the test server: a client leaving mid-statement hung the process
|
|
639
909
|
* for good). A scalar function runs on the thread that registered it — the executor worker (src/duck/executor.ts) —
|
|
640
910
|
* 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.
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* stopped (src/duck/store.ts `cleanUp`; a deadlock under load). The table is shared memory: the listener's thread writes it
|
|
911
|
+
* connection does, to cancel the executor's tasks. So such a statement is cancelled through its session as well as
|
|
912
|
+
* interrupted: its session's cell of the CANCEL TABLE (src/duck/cancel.ts) says it is cancelled, the statement's next call
|
|
913
|
+
* raises, and its result is closed only once DuckDB ended the query off this thread (src/duck/store.ts `cleanUp`; a
|
|
914
|
+
* deadlock under load). The table is shared memory: the listener's thread writes it
|
|
646
915
|
* while this one is busy with the statement's calls. The rewrite (src/duck/rewrite.ts `fn`) passes
|
|
647
916
|
* `getvariable('__mason_session')` as every `__mason_*` function's first argument, which DuckDB folds to the session's
|
|
648
|
-
* literal at bind time; the
|
|
649
|
-
*
|
|
917
|
+
* literal at bind time; the one set-up of a session's connection sets it (src/duck/store.ts `SessionSetup`: a wire
|
|
918
|
+
* session's slot, `s<n>`, or a generated table's run). A connection without it passes NULL, and every Mason function
|
|
919
|
+
* answers NULL for it (DuckDB's NULL in, NULL out): a plan's SQL runs only on a connection `SessionSetup` set up.
|
|
650
920
|
*/
|
|
651
921
|
export const SESSION_VARIABLE = "__mason_session";
|
|
922
|
+
/** the function a thread passes through before a Mason function's vector (`registerTurn`) */
|
|
923
|
+
export const TURN_FUNCTION = "__mason_turn";
|
|
924
|
+
/** the session's settings, name to value, as `current_setting(name)` answers them (src/api/postgres/server.ts
|
|
925
|
+
* `duckSession`): set on a session's connection before each statement that finds them changed (src/duck/store.ts
|
|
926
|
+
* `settle`), read by the rewrite's `settingRead` (src/duck/rewrite.ts) */
|
|
927
|
+
export const SETTINGS_VARIABLE = "__mason_settings";
|
|
928
|
+
/** what parts a `__mason_pg_*` call's first argument, the session's zone then its DateStyle (src/duck/rewrite.ts pgCall) */
|
|
929
|
+
export const PG_SESSION_SEPARATOR = "\u0001";
|
|
930
|
+
/** a Mason function's error as DuckDB carries it: Postgres's SQLSTATE, message and DETAIL where the function says them
|
|
931
|
+
* (src/duck/store.ts postgresWording reads the prefix and the DETAIL's line), else its message */
|
|
932
|
+
const raised = (e) => e instanceof PgEvalError
|
|
933
|
+
? `mason[${e.code}]: ${e.message}${e.detail === undefined ? "" : `\nDETAIL: ${e.detail}`}` : `mason: ${e.message}`;
|
|
652
934
|
export const CANCELLED = "mason[57014]: canceling statement due to user request";
|
|
653
935
|
/** how many vectors each function was called with (a call is a hop to the main thread): what a test or a measurement
|
|
654
936
|
* reads to know whether a statement reached a function at all (the rewrite answers a `->>` natively where it can) */
|
|
@@ -660,29 +942,79 @@ export const FUNCTION_ROWS = new Map();
|
|
|
660
942
|
let cancels = cancelTable();
|
|
661
943
|
export function useCancelTable(t) { cancels = t; }
|
|
662
944
|
export function cancelsTable() { return cancels; }
|
|
663
|
-
/**
|
|
664
|
-
*
|
|
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 */
|
|
945
|
+
/** the session's vector (the first argument, a constant): raises when the session's statement is cancelled. Every call
|
|
946
|
+
* starts here, so every call beats (src/duck/cancel.ts) */
|
|
669
947
|
function cancelled(info, vectors, rows) {
|
|
948
|
+
beat(cancels);
|
|
670
949
|
const slot = rows > 0 ? slotOf(text(vectors[0]?.getItem(0))) : null;
|
|
671
|
-
if (slot !== null)
|
|
672
|
-
lastCallAt[slot] = performance.now();
|
|
673
950
|
if (slot === null || !isCancelled(cancels, slot))
|
|
674
951
|
return false;
|
|
675
952
|
info.setError(CANCELLED);
|
|
676
953
|
return true;
|
|
677
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* THE TURN (issue 127: `select 1` from ten rows took 22 s beside a `to_char` over 17 M rows, 57 s on a real deployment).
|
|
957
|
+
* Every Mason function is a thread-safe function of Node's: DuckDB's threads queue a call and wait for this thread, and
|
|
958
|
+
* Node runs a queue until it is EMPTY, or for 1,000 calls, before its event loop turns (measured: 999 calls between two
|
|
959
|
+
* turns, 2.2 to 3.3 s each). With two or more DuckDB threads calling, the queue is never empty, and a statement of any
|
|
960
|
+
* size needs about eight turns of this loop (settle, prepare, stream, fetch, close, each a libuv completion).
|
|
961
|
+
*
|
|
962
|
+
* This function is the way out of that queue: a thread calls it, on a queue of its OWN, before each vector of a Mason
|
|
963
|
+
* function (src/duck/rewrite.ts `giveTurns`). A thread that finished a call now waits on this queue, which is served
|
|
964
|
+
* only once the running queue has returned to the loop, so no thread can join the running queue while it runs. It
|
|
965
|
+
* empties after the calls already in it, at most one per DuckDB thread, and the loop turns: 10 calls a turn where
|
|
966
|
+
* there were 999, whatever the statements running. It costs one hop a vector and writes nothing per row: the session is
|
|
967
|
+
* handed on as a constant vector, the first argument of the call it guards. Reading the session goes through the API's
|
|
968
|
+
* column wrapper, whose cost is not a constant of the vector's size. One Value per
|
|
969
|
+
* session, so `sessions` is bounded by the cancel table's slots.
|
|
970
|
+
*/
|
|
971
|
+
function registerTurn(conn) {
|
|
972
|
+
const turn = DuckDBScalarFunction.create({
|
|
973
|
+
name: TURN_FUNCTION, returnType: VARCHAR, parameterTypes: [VARCHAR, ANY],
|
|
974
|
+
// a NULL argument is no reason to skip the turn (or a NULL session's: a write of the copy, src/duck/kept.ts)
|
|
975
|
+
specialHandling: true, mainFunction: () => undefined,
|
|
976
|
+
});
|
|
977
|
+
const sessions = new Map();
|
|
978
|
+
let allInvalid = new ArrayBuffer(256); // a vector's rows; a larger vector size grows it
|
|
979
|
+
// the bindings' own callback: `mainFunction`'s wrapper builds the output vector's row-by-row writer, which this never uses
|
|
980
|
+
duckdb.scalar_function_set_function(turn.scalar_function, (_info, input, output) => {
|
|
981
|
+
// a hop like any call's: counted, so a test that holds a statement to no hop at all reads the turn's too
|
|
982
|
+
FUNCTION_CALLS.set(TURN_FUNCTION, (FUNCTION_CALLS.get(TURN_FUNCTION) ?? 0) + 1);
|
|
983
|
+
beat(cancels);
|
|
984
|
+
const chunk = new DuckDBDataChunk(input);
|
|
985
|
+
const session = text(chunk.rowCount > 0 ? chunk.getColumnVector(0).getItem(0) : null);
|
|
986
|
+
if (session === null) {
|
|
987
|
+
// a NULL of the output's own type: `create_null_value` is a Value of type SQLNULL, no VARCHAR. Every row's bit
|
|
988
|
+
// cleared by a copy: `vector_get_validity` hands JavaScript no memory a write reaches (tests/duck-json-columns.test.ts)
|
|
989
|
+
const bytes = Math.ceil(chunk.rowCount / 8);
|
|
990
|
+
if (allInvalid.byteLength < bytes)
|
|
991
|
+
allInvalid = new ArrayBuffer(bytes);
|
|
992
|
+
duckdb.vector_ensure_validity_writable(output);
|
|
993
|
+
duckdb.copy_data_to_vector_validity(output, 0, allInvalid, 0, bytes);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
let value = sessions.get(session);
|
|
997
|
+
if (value === undefined) {
|
|
998
|
+
value = duckdb.create_varchar(session);
|
|
999
|
+
sessions.set(session, value);
|
|
1000
|
+
}
|
|
1001
|
+
duckdb.vector_reference_value(output, value);
|
|
1002
|
+
});
|
|
1003
|
+
conn.registerScalarFunction(turn);
|
|
1004
|
+
}
|
|
678
1005
|
/** Registers the scalar functions on the instance (once: every connection of it sees them). */
|
|
679
1006
|
export function registerFunctions(conn) {
|
|
1007
|
+
registerTurn(conn);
|
|
680
1008
|
for (const kind of Object.keys(PG_KINDS))
|
|
681
1009
|
viaPostgres(conn, kind);
|
|
682
1010
|
scalar(conn, "__mason_case", [VARCHAR, BOOLEAN, VARCHAR], VARCHAR, ([s, up, mode]) => pgCase(String(s), up === true, String(mode)));
|
|
683
1011
|
scalar(conn, "__mason_initcap", [VARCHAR, VARCHAR], VARCHAR, ([s, mode]) => initcap(String(s), String(mode)));
|
|
684
1012
|
// (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]) =>
|
|
1013
|
+
scalar(conn, "__mason_json_get", [VARCHAR, VARCHAR, BOOLEAN, BOOLEAN, BOOLEAN], VARCHAR, ([j, s, either, canonical, asText]) => {
|
|
1014
|
+
const path = JSON.parse(String(s));
|
|
1015
|
+
// a path computed per row (a parameter's, a row's elements): NULL, or a NULL element, reads NULL (get_path_all)
|
|
1016
|
+
return path === null || path.includes(null) ? null : jsonGet(String(j), stepsOf(path, either === true), canonical === true, asText === true);
|
|
1017
|
+
});
|
|
686
1018
|
// the same, as a stored JSON path keeps it (keptJsonValue, src/duck/kept.ts): what the copy's writes compute
|
|
687
1019
|
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
1020
|
// a document's rows as a list each (elements, keys, values); `orNull` (the last argument): never empty, a NULL or
|
|
@@ -727,8 +1059,12 @@ export function registerFunctions(conn) {
|
|
|
727
1059
|
// an abbreviation or a POSIX zone, which Postgres reads with a fixed offset and DuckDB as another zone, refused
|
|
728
1060
|
scalar(conn, "__mason_zone", [VARCHAR], VARCHAR, ([z]) => {
|
|
729
1061
|
const read = atTimeZoneOf(String(z));
|
|
730
|
-
if ("problem" in read)
|
|
731
|
-
|
|
1062
|
+
if ("problem" in read) {
|
|
1063
|
+
// the zone is a row's here: Mason's refusal never quotes it (Postgres's own 22023 does, as Postgres does)
|
|
1064
|
+
if (read.sqlstate === "0A000")
|
|
1065
|
+
throw new PgEvalError("0A000", `a time zone read from a row is an abbreviation or a POSIX zone, which Postgres reads with a fixed offset where the IANA zone of that name may move: write an IANA name (${gapMark("zone-abbreviation")})`);
|
|
1066
|
+
throw new PgEvalError(read.sqlstate, read.problem);
|
|
1067
|
+
}
|
|
732
1068
|
return read.zone;
|
|
733
1069
|
});
|
|
734
1070
|
// a division's divisor tested for zero (src/duck/rewrite.ts nonZero): Postgres's error, from a function DuckDB takes
|
|
@@ -754,7 +1090,7 @@ export function registerFunctions(conn) {
|
|
|
754
1090
|
}
|
|
755
1091
|
catch (e) {
|
|
756
1092
|
if (e instanceof NumericError)
|
|
757
|
-
throw
|
|
1093
|
+
throw numericEvalError(e);
|
|
758
1094
|
throw e;
|
|
759
1095
|
}
|
|
760
1096
|
});
|
|
@@ -771,7 +1107,7 @@ export function registerFunctions(conn) {
|
|
|
771
1107
|
const [whole, frac = ""] = s.replace(/^-/, "").split(".");
|
|
772
1108
|
const kept = frac.replace(/0+$/, "");
|
|
773
1109
|
if (whole.replace(/^0+(?=\d)/, "").length > 20 || kept.length > 36)
|
|
774
|
-
throw new PgEvalError("0A000", `
|
|
1110
|
+
throw new PgEvalError("0A000", `a sum of a numeric past 20 integer or 36 fractional digits is not supported by Mason (${gapMark("numeric-exact-sum")})`);
|
|
775
1111
|
return part === 0 ? `${sign}${whole}.${kept.slice(0, 18) || "0"}` : `${sign}0.${kept.slice(18) || "0"}`;
|
|
776
1112
|
});
|
|
777
1113
|
// a text read as an enum (src/duck/rewrite.ts enumValue): the label, or Postgres's error for one the type lacks
|
|
@@ -782,19 +1118,81 @@ export function registerFunctions(conn) {
|
|
|
782
1118
|
return label;
|
|
783
1119
|
});
|
|
784
1120
|
scalar(conn, "__mason_numeric_special", [VARCHAR], BOOLEAN, ([t]) => !/^-?\d/.test(String(t)));
|
|
1121
|
+
// a numeric's EQUALITY KEY (src/duck/rewrite.ts exactKey): its exact text without trailing zeros, so two values
|
|
1122
|
+
// Postgres finds equal (5.0 and 5.00, 0 and -0.0) are one key and two it tells apart are two, whatever their doubles
|
|
1123
|
+
scalar(conn, "__mason_numeric_key", [VARCHAR], VARCHAR, ([t]) => numericKey(String(t)));
|
|
785
1124
|
// a numeric program over a row's cells' texts (src/duck/rewrite.ts exactText): Postgres's exact value as numeric_out
|
|
786
1125
|
// writes it, where DuckDB computes a double (a quotient handed to ln)
|
|
1126
|
+
// The program is its one text (src/lib/pg-numeric.ts `programJson`, each shared node once), the same literal on every
|
|
1127
|
+
// row: parsed once per text (`parsedProgram`), and each node computed at most once a row
|
|
787
1128
|
scalar(conn, "__mason_numeric_program", [], VARCHAR, ([program, ...cells]) => {
|
|
788
1129
|
const values = Object.fromEntries(cells.map((c, i) => [String(i), text(c)]));
|
|
789
1130
|
try {
|
|
790
|
-
return evaluateText(
|
|
1131
|
+
return evaluateText(parsedProgram(String(program)), values);
|
|
791
1132
|
}
|
|
792
1133
|
catch (e) {
|
|
793
1134
|
if (e instanceof NumericError)
|
|
794
|
-
throw
|
|
1135
|
+
throw numericEvalError(e);
|
|
795
1136
|
throw e;
|
|
796
1137
|
}
|
|
797
1138
|
}, { nulls: "pass", varargs: true });
|
|
1139
|
+
programKeyFunction(conn);
|
|
1140
|
+
// WHETHER THE ENGINE'S TYPE HOLDS A BOUND NUMERIC EXACTLY (src/duck/rewrite.ts numericParamComparison, issue 214):
|
|
1141
|
+
// DuckDB's name of the compared side's type (`typeof`) and the parameter's numeric_out text, both constants DuckDB
|
|
1142
|
+
// folds at Execute, so this is called once a statement and the comparison it guards is the scan's own filter. A NULL
|
|
1143
|
+
// is held by every type (the comparison's own NULL is Postgres's)
|
|
1144
|
+
scalar(conn, "__mason_numeric_fits", [VARCHAR, VARCHAR], BOOLEAN, ([type, t]) => t === null || t === undefined || fitsEngineType(String(type), String(t)), { nulls: "pass" });
|
|
1145
|
+
// a numeric's ORDER KEY (src/duck/rewrite.ts exactOrderKey, issue 252): bytes that sort as numeric_cmp, so a program's
|
|
1146
|
+
// value is ordered, ranked and tied by its exact value, never by its double (1.00000000000000000001 and …02 are two)
|
|
1147
|
+
scalar(conn, "__mason_numeric_order", [VARCHAR], BLOB, ([t]) => {
|
|
1148
|
+
try {
|
|
1149
|
+
return new DuckDBBlobValue(Buffer.from(numericOrderKey(String(t)), "latin1"));
|
|
1150
|
+
}
|
|
1151
|
+
catch (e) {
|
|
1152
|
+
if (e instanceof NumericError)
|
|
1153
|
+
throw numericEvalError(e);
|
|
1154
|
+
throw e;
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
// a numeric's exact text cast to an integer of `bits` (numeric_int4 …): rounded half away from zero, 22003 past its range
|
|
1158
|
+
scalar(conn, "__mason_numeric_int", [VARCHAR, INTEGER], VARCHAR, ([t, bits]) => {
|
|
1159
|
+
try {
|
|
1160
|
+
return numericToInteger(String(t), Number(bits));
|
|
1161
|
+
}
|
|
1162
|
+
catch (e) {
|
|
1163
|
+
if (e instanceof NumericError)
|
|
1164
|
+
throw numericEvalError(e);
|
|
1165
|
+
throw e;
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
// A NUMERIC WITH NO PROGRAM cast to an integer (issue 242), decided by the type the engine holds it in (`typeof`): a
|
|
1169
|
+
// DECIMAL's or a text's digits are exact, rounded as numeric_int4 rounds them; a double's would round half to even
|
|
1170
|
+
// where Postgres rounds the exact value half away from zero, and bytes are no number: each refused by name
|
|
1171
|
+
scalar(conn, "__mason_numeric_int_of", [VARCHAR, VARCHAR, INTEGER], VARCHAR, ([type, t, bits]) => {
|
|
1172
|
+
if (/^(DOUBLE|FLOAT|REAL|BLOB)$/i.test(String(type))) {
|
|
1173
|
+
throw new PgEvalError("0A000", `a numeric the engine holds as a double, which Mason cannot compute exactly here, cast to an integer type is not supported by Mason (${gapMark("numeric-double-integer")}): Postgres rounds the exact value half away from zero, the double would round half to even; cast it to float8 first, (stddev(x)::float8)::int, which rounds the double as Postgres rounds a float8`);
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
return numericToInteger(String(t), Number(bits));
|
|
1177
|
+
}
|
|
1178
|
+
catch (e) {
|
|
1179
|
+
if (e instanceof NumericError)
|
|
1180
|
+
throw numericEvalError(e);
|
|
1181
|
+
throw e;
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
// a numeric's exact text cast to float8 (numeric_float8: strtod of numeric_out's text): the double nearest it, 22003
|
|
1185
|
+
// where that is past a double's range or a non-zero value underflows to 0 (issue 217)
|
|
1186
|
+
scalar(conn, "__mason_numeric_double", [VARCHAR], DOUBLE, ([t]) => {
|
|
1187
|
+
try {
|
|
1188
|
+
return numericToDouble(String(t));
|
|
1189
|
+
}
|
|
1190
|
+
catch (e) {
|
|
1191
|
+
if (e instanceof NumericError)
|
|
1192
|
+
throw numericEvalError(e);
|
|
1193
|
+
throw e;
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
798
1196
|
// the decimals of numeric_out's text: the value's display scale (`2.450` has 3)
|
|
799
1197
|
scalar(conn, "__mason_numeric_scale", [VARCHAR], INTEGER, ([t]) => { const at = String(t).indexOf("."); return at < 0 ? 0 : String(t).length - at - 1; });
|
|
800
1198
|
scalar(conn, "__mason_float_numeric_text", [DOUBLE, BOOLEAN], VARCHAR, ([x, four]) => floatNumericText(Number(x), four === true));
|