@mason-data/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +13 -4
  2. package/dist/src/api/postgres/catalog-worker.js +49 -2
  3. package/dist/src/api/postgres/catalog.js +166 -32
  4. package/dist/src/api/postgres/listener.js +11 -3
  5. package/dist/src/api/postgres/proxy.js +283 -0
  6. package/dist/src/api/postgres/schema.js +6 -0
  7. package/dist/src/api/postgres/server.js +505 -99
  8. package/dist/src/api/postgres/translate.js +47 -2
  9. package/dist/src/cli/background.js +18 -19
  10. package/dist/src/cli/bridge-stdin.js +101 -0
  11. package/dist/src/cli/command.js +169 -11
  12. package/dist/src/cli/commands.js +207 -51
  13. package/dist/src/cli/credentials.js +182 -0
  14. package/dist/src/cli/demo.js +2 -2
  15. package/dist/src/cli/deploy.js +574 -0
  16. package/dist/src/cli/dev.js +148 -27
  17. package/dist/src/cli/doctor.js +116 -33
  18. package/dist/src/cli/generated-files.js +143 -0
  19. package/dist/src/cli/guides.js +97 -6
  20. package/dist/src/cli/hosted.js +850 -0
  21. package/dist/src/cli/init.js +47 -11
  22. package/dist/src/cli/login.js +289 -0
  23. package/dist/src/cli/machine.js +1 -1
  24. package/dist/src/cli/main.js +8 -2
  25. package/dist/src/cli/mason.cjs +17 -0
  26. package/dist/src/cli/mcp.js +2 -2
  27. package/dist/src/cli/output-gate.js +176 -0
  28. package/dist/src/cli/page-share.js +96 -0
  29. package/dist/src/cli/pg-tls.js +1 -1
  30. package/dist/src/cli/pin.js +6 -2
  31. package/dist/src/cli/pool-size.cjs +85 -0
  32. package/dist/src/cli/ports.js +2 -0
  33. package/dist/src/cli/preflight.js +130 -39
  34. package/dist/src/cli/project.js +99 -0
  35. package/dist/src/cli/readiness.js +42 -6
  36. package/dist/src/cli/registry.js +12 -4
  37. package/dist/src/cli/replica-hosted.js +665 -0
  38. package/dist/src/cli/replica.js +76 -0
  39. package/dist/src/cli/roles.js +2 -1
  40. package/dist/src/cli/run-server.js +154 -7
  41. package/dist/src/cli/security.js +57 -10
  42. package/dist/src/cli/serve.js +63 -7
  43. package/dist/src/cli/share-run.js +51 -8
  44. package/dist/src/cli/share.js +3 -3
  45. package/dist/src/cli/source-edit.js +10 -4
  46. package/dist/src/cli/source-role.js +344 -0
  47. package/dist/src/cli/sources.js +30 -20
  48. package/dist/src/cli/style.js +2 -1
  49. package/dist/src/cli/supervise.js +15 -23
  50. package/dist/src/cli/token-hosted.js +337 -0
  51. package/dist/src/cli/ui-ssh.js +460 -0
  52. package/dist/src/cli/ui.js +115 -11
  53. package/dist/src/cli/user-hosted.js +299 -0
  54. package/dist/src/cli/user.js +126 -74
  55. package/dist/src/demo/seed.js +26 -0
  56. package/dist/src/duck/analyze.js +36 -20
  57. package/dist/src/duck/collation.js +266 -0
  58. package/dist/src/duck/constant-key.js +94 -0
  59. package/dist/src/duck/distinct-key.js +283 -26
  60. package/dist/src/duck/events.js +39 -8
  61. package/dist/src/duck/executor-worker.js +2 -2
  62. package/dist/src/duck/executor.js +6 -5
  63. package/dist/src/duck/explain.js +4 -0
  64. package/dist/src/duck/functions.js +448 -49
  65. package/dist/src/duck/gaps.js +34 -16
  66. package/dist/src/duck/generated-plan.js +1070 -0
  67. package/dist/src/duck/generated.js +906 -0
  68. package/dist/src/duck/grants.js +1 -1
  69. package/dist/src/duck/held-types.js +64 -0
  70. package/dist/src/duck/history.js +4 -0
  71. package/dist/src/duck/jsonb-order.js +9 -12
  72. package/dist/src/duck/kept-route.js +6 -5
  73. package/dist/src/duck/kept.js +9 -7
  74. package/dist/src/duck/late.js +1469 -40
  75. package/dist/src/duck/libc-bound.js +341 -0
  76. package/dist/src/duck/libc-order.js +892 -126
  77. package/dist/src/duck/pg-eval-worker.js +7 -1
  78. package/dist/src/duck/pg-eval.js +6 -4
  79. package/dist/src/duck/pgtree.js +774 -23
  80. package/dist/src/duck/plan.js +275 -30
  81. package/dist/src/duck/publication.js +14 -0
  82. package/dist/src/duck/requests.js +9 -5
  83. package/dist/src/duck/resolved.js +117 -2
  84. package/dist/src/duck/rewrite.js +4567 -583
  85. package/dist/src/duck/row-filter.js +12 -6
  86. package/dist/src/duck/statement.js +19 -0
  87. package/dist/src/duck/store.js +294 -68
  88. package/dist/src/duck/stream.js +9 -5
  89. package/dist/src/duck/sync.js +602 -126
  90. package/dist/src/duck/token.js +4 -1
  91. package/dist/src/duck/types.js +63 -7
  92. package/dist/src/duck/usage.js +6 -4
  93. package/dist/src/duck/users.js +5 -0
  94. package/dist/src/duck/verified.js +159 -2
  95. package/dist/src/duck/views.js +4 -3
  96. package/dist/src/lib/database-url.js +55 -10
  97. package/dist/src/lib/instant.js +50 -0
  98. package/dist/src/lib/log-buffer.js +13 -4
  99. package/dist/src/lib/log-read.js +196 -0
  100. package/dist/src/lib/pg-numeric.js +387 -49
  101. package/dist/src/lib/secret-mask.js +142 -0
  102. package/dist/src/lib/table-sync.js +24 -9
  103. package/dist/src/web/app.js +9 -4
  104. package/dist/src/web/pages/status.js +178 -13
  105. package/dist/src/web/pages/syncs.js +1 -1
  106. package/dist/src/web/pages/tables.js +8 -3
  107. package/dist/src/web/pages/users.js +4 -3
  108. package/dist/src/web/shared.js +19 -3
  109. 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 { BIGINT, BLOB, BOOLEAN, DATE, DOUBLE, DuckDBBlobValue, DuckDBDateValue, DuckDBIntervalValue, DuckDBScalarFunction, DuckDBTimestampTZValue, DuckDBTimestampValue, INTEGER, INTERVAL, LIST, TIMESTAMP, TIMESTAMPTZ, VARCHAR, listValue } from "@duckdb/node-api";
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 { libcKept, libcKey } from "./libc-order.js";
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, jsonbLibcKept, jsonbStrings, STORED_SUFFIX } from "./jsonb-order.js";
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
- /** one character's simple mapping, as towupper / towlower give it: a character whose full mapping is several (`ß`,
29
- * `ʼn`) keeps itself */
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
- const ASCII_ALNUM = /^[A-Za-z0-9]$/;
35
- const ALNUM = /^[\p{L}\p{N}]$/u;
36
- /** upper() / lower() as Postgres computes them under that mode (libc's upper is the rewrite's SQL, not this) */
37
- export function pgCase(s, upper, mode) {
38
- if (mode === "icu")
39
- return upper ? s.toUpperCase() : s.toLowerCase();
40
- if (mode === "ascii")
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
- let out = "";
43
- for (const ch of s)
44
- out += simple(ch, upper);
45
- return out;
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
- /** initcap(): a letter after a letter or a digit is lowered, every other is raised (Postgres's str_initcap) */
48
- export function initcap(s, mode) {
49
- let out = "", was = false;
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 (mode === "ascii")
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 = (mode === "ascii" ? ASCII_ALNUM : ALNUM).test(ch);
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 instanceof PgEvalError ? `mason[${e.code}]: ${e.message}` : `mason: ${e.message}`);
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 (${t}) is not supported by Mason (Mason gap 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", `the text '${relative}' read as a time is not supported by Mason: it means the transaction's start (Mason gap relative-time-literal)`);
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 got = pgEvalSync(sql, cols.map((_, i) => tuples.map((t) => t[i])), g.zone);
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 instanceof PgEvalError ? `mason[${e.code}]: ${e.message}` : `mason: ${e.message}`);
853
+ info.setError(raised(e));
599
854
  }
600
855
  },
601
856
  }));
602
857
  }
603
- const steps = (json, either) => JSON.parse(json)
604
- .map((s) => typeof s === "number" ? { index: s } : either ? { either: s } : { key: s });
605
- /** The libc order's two functions (src/duck/libc-order.ts), for the order the source was read with: the characters
606
- * ICU's accent and case levels read, and the key (a value holding one not covered refuses, by name). */
607
- export function registerLibcOrder(conn, order) {
608
- scalar(conn, "__mason_libc_kept", [VARCHAR], VARCHAR, ([s]) => libcKept(order, String(s)));
609
- scalar(conn, "__mason_libc_key", [VARCHAR, VARCHAR], VARCHAR, ([s, icu]) => libcKey(order, String(s), String(icu)));
610
- // jsonb's order under it (src/duck/jsonb-order.ts): the characters of each string ICU's levels read, then the key
611
- // from ICU's keys of them (src/duck/rewrite.ts jsonbKey hands them in)
612
- keyScalar(conn, JSONB_KEY_FUNCTIONS.libc.strings, [VARCHAR], LIST(VARCHAR), ([j]) => listValue(jsonbLibcKept(order, String(j))));
613
- keyScalar(conn, JSONB_KEY_FUNCTIONS.libc.key, [VARCHAR, LIST(VARCHAR)], BLOB, ([j, keys]) => new DuckDBBlobValue(jsonbKey(String(j), { kind: "libc", order, icuKeys: icuKeysOf(keys) })));
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 session runner sets the variable on its connection (its slot, `s<n>`). A connection without
649
- * it (the writer, a copy's) passes NULL: never cancelled this way.
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]) => jsonGet(String(j), steps(String(s), either === true), canonical === true, asText === true));
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
- throw new PgEvalError(read.sqlstate, read.sqlstate === "0A000" ? `${read.problem} (${gapMark("zone-abbreviation")})` : read.problem);
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 new PgEvalError(e.sqlstate, e.message);
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", `the sum of the numeric ${s} is not supported by Mason (${gapMark("numeric-exact-sum")})`);
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(JSON.parse(String(program)), values);
1131
+ return evaluateText(parsedProgram(String(program)), values);
790
1132
  }
791
1133
  catch (e) {
792
1134
  if (e instanceof NumericError)
793
- throw new PgEvalError(e.sqlstate, e.message);
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));