@mason-data/cli 0.8.0 → 0.10.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 +19 -8
- package/dist/src/api/error.js +4 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +166 -32
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/proxy.js +129 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +527 -103
- package/dist/src/api/postgres/translate.js +47 -2
- package/dist/src/cli/background.js +18 -19
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/command.js +337 -15
- package/dist/src/cli/commands.js +458 -169
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +2 -2
- package/dist/src/cli/deploy.js +680 -0
- package/dist/src/cli/dev.js +148 -27
- package/dist/src/cli/doctor.js +122 -36
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +116 -16
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +154 -33
- package/dist/src/cli/login.js +292 -0
- package/dist/src/cli/machine.js +125 -40
- package/dist/src/cli/main.js +59 -10
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +17 -14
- 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 +1 -1
- package/dist/src/cli/pin.js +6 -2
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +130 -39
- package/dist/src/cli/project.js +100 -1
- package/dist/src/cli/readiness.js +42 -6
- package/dist/src/cli/registry.js +89 -36
- package/dist/src/cli/replica-hosted.js +1324 -0
- package/dist/src/cli/replica.js +114 -0
- package/dist/src/cli/replication-mode.js +5 -0
- package/dist/src/cli/request-filter.js +16 -6
- package/dist/src/cli/roles.js +2 -1
- package/dist/src/cli/run-server.js +179 -9
- package/dist/src/cli/secret-input.js +193 -0
- package/dist/src/cli/security.js +57 -10
- package/dist/src/cli/serve.js +65 -9
- package/dist/src/cli/share-run.js +51 -8
- package/dist/src/cli/share.js +23 -18
- package/dist/src/cli/source-edit.js +10 -4
- package/dist/src/cli/source-role.js +343 -0
- package/dist/src/cli/sources.js +50 -23
- package/dist/src/cli/style.js +16 -1
- package/dist/src/cli/supervise.js +15 -23
- package/dist/src/cli/sync.js +17 -8
- package/dist/src/cli/token-hosted.js +343 -0
- package/dist/src/cli/tunnel.js +1 -1
- package/dist/src/cli/ui-hosted.js +163 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +162 -17
- package/dist/src/cli/usage.js +19 -15
- package/dist/src/cli/user-hosted.js +364 -0
- package/dist/src/cli/user.js +275 -105
- package/dist/src/demo/seed.js +26 -0
- package/dist/src/duck/analyze.js +36 -20
- 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 +39 -8
- package/dist/src/duck/executor-worker.js +2 -2
- package/dist/src/duck/executor.js +6 -5
- package/dist/src/duck/explain.js +4 -0
- package/dist/src/duck/functions.js +448 -49
- package/dist/src/duck/gaps.js +34 -16
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +1 -1
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +4 -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 +9 -7
- 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 +275 -30
- package/dist/src/duck/publication.js +14 -0
- package/dist/src/duck/requests.js +9 -5
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4567 -583
- package/dist/src/duck/row-filter.js +12 -6
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +294 -68
- package/dist/src/duck/stream.js +9 -5
- package/dist/src/duck/sync.js +602 -126
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +63 -7
- package/dist/src/duck/usage.js +6 -4
- package/dist/src/duck/users.js +110 -9
- package/dist/src/duck/verified.js +159 -2
- package/dist/src/duck/views.js +4 -3
- package/dist/src/lib/cidr.js +177 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +13 -4
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +387 -49
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +84 -19
- package/dist/src/web/app.js +9 -4
- package/dist/src/web/pages/status.js +178 -13
- package/dist/src/web/pages/syncs.js +1 -1
- package/dist/src/web/pages/tables.js +8 -3
- package/dist/src/web/pages/users.js +4 -3
- package/dist/src/web/shared.js +19 -3
- package/package.json +8 -7
|
@@ -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
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 = {}) {
|
|
@@ -505,7 +683,83 @@ function scalar(conn, name, params, returns, body, opts = {}) {
|
|
|
505
683
|
catch (e) {
|
|
506
684
|
// Postgres's own message where the function raises one (src/duck/store.ts postgresWording reads the prefix), and
|
|
507
685
|
// its SQLSTATE where the function says it
|
|
508
|
-
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));
|
|
509
763
|
}
|
|
510
764
|
},
|
|
511
765
|
}));
|
|
@@ -537,7 +791,7 @@ export const PG_KINDS = {
|
|
|
537
791
|
read: (t) => {
|
|
538
792
|
const parts = t.split(" ");
|
|
539
793
|
if (parts.length !== 3)
|
|
540
|
-
throw new PgEvalError("0A000", `an infinite interval
|
|
794
|
+
throw new PgEvalError("0A000", `an infinite interval is not supported by Mason (${gapMark("infinite-interval")})`);
|
|
541
795
|
return new DuckDBIntervalValue(Number(parts[0]), Number(parts[1]), BigInt(parts[2]));
|
|
542
796
|
},
|
|
543
797
|
},
|
|
@@ -578,13 +832,14 @@ function viaPostgres(conn, kind) {
|
|
|
578
832
|
if (kind === "date" || kind === "timestamp" || kind === "timestamptz" || READS_TIME.test(g.template)) {
|
|
579
833
|
const relative = tuples.flat().find((a) => a !== null && RELATIVE.test(a));
|
|
580
834
|
if (relative !== undefined)
|
|
581
|
-
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")})`);
|
|
582
836
|
}
|
|
583
837
|
const n = tuples[0].length;
|
|
584
838
|
const cols = Array.from({ length: n }, (_, i) => `a${i + 1}`);
|
|
585
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) ` +
|
|
586
840
|
`CROSS JOIN LATERAL (SELECT ${g.template} AS mason_x) AS mason_s ORDER BY mason_n`;
|
|
587
|
-
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);
|
|
588
843
|
got.forEach(([v], i) => answers.set(`${key}\u0000${JSON.stringify(tuples[i])}`, v ?? null));
|
|
589
844
|
}
|
|
590
845
|
rows.forEach(([zone = null, template = null, ...args], r) => {
|
|
@@ -595,22 +850,36 @@ function viaPostgres(conn, kind) {
|
|
|
595
850
|
}
|
|
596
851
|
catch (e) {
|
|
597
852
|
// Postgres's SQLSTATE and words (src/duck/store.ts postgresWording reads them back)
|
|
598
|
-
info.setError(e
|
|
853
|
+
info.setError(raised(e));
|
|
599
854
|
}
|
|
600
855
|
},
|
|
601
856
|
}));
|
|
602
857
|
}
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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 })));
|
|
614
883
|
}
|
|
615
884
|
/**
|
|
616
885
|
* A FUNCTION OF A JSONB'S KEY (src/duck/jsonb-order.ts `JSONB_KEY_FUNCTIONS`), and its stored twin (`STORED_SUFFIX`),
|
|
@@ -645,14 +914,23 @@ function icuKeysOf(v) {
|
|
|
645
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";
|
|
652
924
|
/** the session's settings, name to value, as `current_setting(name)` answers them (src/api/postgres/server.ts
|
|
653
925
|
* `duckSession`): set on a session's connection before each statement that finds them changed (src/duck/store.ts
|
|
654
926
|
* `settle`), read by the rewrite's `settingRead` (src/duck/rewrite.ts) */
|
|
655
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}`;
|
|
656
934
|
export const CANCELLED = "mason[57014]: canceling statement due to user request";
|
|
657
935
|
/** how many vectors each function was called with (a call is a hop to the main thread): what a test or a measurement
|
|
658
936
|
* reads to know whether a statement reached a function at all (the rewrite answers a `->>` natively where it can) */
|
|
@@ -674,14 +952,69 @@ function cancelled(info, vectors, rows) {
|
|
|
674
952
|
info.setError(CANCELLED);
|
|
675
953
|
return true;
|
|
676
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
|
+
}
|
|
677
1005
|
/** Registers the scalar functions on the instance (once: every connection of it sees them). */
|
|
678
1006
|
export function registerFunctions(conn) {
|
|
1007
|
+
registerTurn(conn);
|
|
679
1008
|
for (const kind of Object.keys(PG_KINDS))
|
|
680
1009
|
viaPostgres(conn, kind);
|
|
681
1010
|
scalar(conn, "__mason_case", [VARCHAR, BOOLEAN, VARCHAR], VARCHAR, ([s, up, mode]) => pgCase(String(s), up === true, String(mode)));
|
|
682
1011
|
scalar(conn, "__mason_initcap", [VARCHAR, VARCHAR], VARCHAR, ([s, mode]) => initcap(String(s), String(mode)));
|
|
683
1012
|
// (json, steps as a JSON array, a #> path's, jsonb's canonical text, ->>'s text)
|
|
684
|
-
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
|
+
});
|
|
685
1018
|
// the same, as a stored JSON path keeps it (keptJsonValue, src/duck/kept.ts): what the copy's writes compute
|
|
686
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));
|
|
687
1020
|
// a document's rows as a list each (elements, keys, values); `orNull` (the last argument): never empty, a NULL or
|
|
@@ -726,8 +1059,12 @@ export function registerFunctions(conn) {
|
|
|
726
1059
|
// an abbreviation or a POSIX zone, which Postgres reads with a fixed offset and DuckDB as another zone, refused
|
|
727
1060
|
scalar(conn, "__mason_zone", [VARCHAR], VARCHAR, ([z]) => {
|
|
728
1061
|
const read = atTimeZoneOf(String(z));
|
|
729
|
-
if ("problem" in read)
|
|
730
|
-
|
|
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
|
+
}
|
|
731
1068
|
return read.zone;
|
|
732
1069
|
});
|
|
733
1070
|
// a division's divisor tested for zero (src/duck/rewrite.ts nonZero): Postgres's error, from a function DuckDB takes
|
|
@@ -753,7 +1090,7 @@ export function registerFunctions(conn) {
|
|
|
753
1090
|
}
|
|
754
1091
|
catch (e) {
|
|
755
1092
|
if (e instanceof NumericError)
|
|
756
|
-
throw
|
|
1093
|
+
throw numericEvalError(e);
|
|
757
1094
|
throw e;
|
|
758
1095
|
}
|
|
759
1096
|
});
|
|
@@ -770,7 +1107,7 @@ export function registerFunctions(conn) {
|
|
|
770
1107
|
const [whole, frac = ""] = s.replace(/^-/, "").split(".");
|
|
771
1108
|
const kept = frac.replace(/0+$/, "");
|
|
772
1109
|
if (whole.replace(/^0+(?=\d)/, "").length > 20 || kept.length > 36)
|
|
773
|
-
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")})`);
|
|
774
1111
|
return part === 0 ? `${sign}${whole}.${kept.slice(0, 18) || "0"}` : `${sign}0.${kept.slice(18) || "0"}`;
|
|
775
1112
|
});
|
|
776
1113
|
// a text read as an enum (src/duck/rewrite.ts enumValue): the label, or Postgres's error for one the type lacks
|
|
@@ -781,19 +1118,81 @@ export function registerFunctions(conn) {
|
|
|
781
1118
|
return label;
|
|
782
1119
|
});
|
|
783
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)));
|
|
784
1124
|
// a numeric program over a row's cells' texts (src/duck/rewrite.ts exactText): Postgres's exact value as numeric_out
|
|
785
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
|
|
786
1128
|
scalar(conn, "__mason_numeric_program", [], VARCHAR, ([program, ...cells]) => {
|
|
787
1129
|
const values = Object.fromEntries(cells.map((c, i) => [String(i), text(c)]));
|
|
788
1130
|
try {
|
|
789
|
-
return evaluateText(
|
|
1131
|
+
return evaluateText(parsedProgram(String(program)), values);
|
|
790
1132
|
}
|
|
791
1133
|
catch (e) {
|
|
792
1134
|
if (e instanceof NumericError)
|
|
793
|
-
throw
|
|
1135
|
+
throw numericEvalError(e);
|
|
794
1136
|
throw e;
|
|
795
1137
|
}
|
|
796
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
|
+
});
|
|
797
1196
|
// the decimals of numeric_out's text: the value's display scale (`2.450` has 3)
|
|
798
1197
|
scalar(conn, "__mason_numeric_scale", [VARCHAR], INTEGER, ([t]) => { const at = String(t).indexOf("."); return at < 0 ? 0 : String(t).length - at - 1; });
|
|
799
1198
|
scalar(conn, "__mason_float_numeric_text", [DOUBLE, BOOLEAN], VARCHAR, ([x, four]) => floatNumericText(Number(x), four === true));
|