@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.
Files changed (155) hide show
  1. package/README.md +101 -73
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +368 -25
  5. package/dist/src/api/postgres/encode.js +3 -1
  6. package/dist/src/api/postgres/explain.js +3 -2
  7. package/dist/src/api/postgres/listener.js +11 -3
  8. package/dist/src/api/postgres/protocol.js +39 -23
  9. package/dist/src/api/postgres/proxy.js +283 -0
  10. package/dist/src/api/postgres/schema.js +6 -0
  11. package/dist/src/api/postgres/server.js +1617 -245
  12. package/dist/src/api/postgres/translate.js +127 -10
  13. package/dist/src/api/request-log.js +18 -5
  14. package/dist/src/cdc/poll.js +5 -5
  15. package/dist/src/cli/background.js +47 -25
  16. package/dist/src/cli/bridge-stdin.js +101 -0
  17. package/dist/src/cli/cancel.js +75 -0
  18. package/dist/src/cli/command.js +170 -11
  19. package/dist/src/cli/commands.js +804 -209
  20. package/dist/src/cli/control.js +59 -0
  21. package/dist/src/cli/credentials.js +182 -0
  22. package/dist/src/cli/demo.js +20 -11
  23. package/dist/src/cli/deploy.js +574 -0
  24. package/dist/src/cli/dev.js +491 -130
  25. package/dist/src/cli/doctor.js +131 -38
  26. package/dist/src/cli/generated-files.js +143 -0
  27. package/dist/src/cli/guides.js +175 -36
  28. package/dist/src/cli/hosted.js +850 -0
  29. package/dist/src/cli/init.js +162 -65
  30. package/dist/src/cli/login.js +289 -0
  31. package/dist/src/cli/machine.js +13 -12
  32. package/dist/src/cli/main.js +11 -5
  33. package/dist/src/cli/mason.cjs +17 -0
  34. package/dist/src/cli/mcp.js +31 -22
  35. package/dist/src/cli/metrics.js +79 -0
  36. package/dist/src/cli/output-gate.js +176 -0
  37. package/dist/src/cli/page-share.js +96 -0
  38. package/dist/src/cli/pg-tls.js +14 -4
  39. package/dist/src/cli/pg-url.js +4 -0
  40. package/dist/src/cli/pin.js +19 -14
  41. package/dist/src/cli/pool-size.cjs +85 -0
  42. package/dist/src/cli/ports.js +2 -0
  43. package/dist/src/cli/preflight.js +138 -46
  44. package/dist/src/cli/project.js +142 -4
  45. package/dist/src/cli/readiness.js +76 -6
  46. package/dist/src/cli/registry.js +65 -38
  47. package/dist/src/cli/replica-hosted.js +665 -0
  48. package/dist/src/cli/replica.js +76 -0
  49. package/dist/src/cli/replication-checks.js +7 -5
  50. package/dist/src/cli/replication-mode.js +105 -26
  51. package/dist/src/cli/request-filter.js +134 -0
  52. package/dist/src/cli/roles.js +585 -0
  53. package/dist/src/cli/run-server.js +637 -48
  54. package/dist/src/cli/runs.js +1 -1
  55. package/dist/src/cli/security.js +67 -18
  56. package/dist/src/cli/serve.js +204 -46
  57. package/dist/src/cli/share-run.js +329 -0
  58. package/dist/src/cli/share.js +286 -0
  59. package/dist/src/cli/source-edit.js +75 -17
  60. package/dist/src/cli/source-role.js +344 -0
  61. package/dist/src/cli/sources.js +306 -47
  62. package/dist/src/cli/style.js +2 -1
  63. package/dist/src/cli/supervise.js +27 -24
  64. package/dist/src/cli/sync.js +55 -25
  65. package/dist/src/cli/token-hosted.js +337 -0
  66. package/dist/src/cli/tunnel.js +200 -0
  67. package/dist/src/cli/ui-ssh.js +460 -0
  68. package/dist/src/cli/ui.js +124 -17
  69. package/dist/src/cli/usage.js +128 -0
  70. package/dist/src/cli/user-hosted.js +299 -0
  71. package/dist/src/cli/user.js +573 -65
  72. package/dist/src/cli/version.js +9 -7
  73. package/dist/src/demo/seed.js +29 -0
  74. package/dist/src/duck/analyze.js +36 -20
  75. package/dist/src/duck/cancel.js +10 -1
  76. package/dist/src/duck/collation.js +266 -0
  77. package/dist/src/duck/constant-key.js +94 -0
  78. package/dist/src/duck/distinct-key.js +283 -26
  79. package/dist/src/duck/events.js +165 -0
  80. package/dist/src/duck/executor-worker.js +20 -11
  81. package/dist/src/duck/executor.js +47 -33
  82. package/dist/src/duck/explain.js +60 -46
  83. package/dist/src/duck/extensions.js +160 -0
  84. package/dist/src/duck/functions.js +461 -63
  85. package/dist/src/duck/gaps.js +48 -21
  86. package/dist/src/duck/generated-plan.js +1070 -0
  87. package/dist/src/duck/generated.js +906 -0
  88. package/dist/src/duck/grants.js +176 -0
  89. package/dist/src/duck/held-types.js +64 -0
  90. package/dist/src/duck/history.js +127 -0
  91. package/dist/src/duck/jsonb-order.js +9 -12
  92. package/dist/src/duck/kept-route.js +6 -5
  93. package/dist/src/duck/kept.js +15 -13
  94. package/dist/src/duck/late.js +1469 -40
  95. package/dist/src/duck/libc-bound.js +341 -0
  96. package/dist/src/duck/libc-order.js +892 -126
  97. package/dist/src/duck/pg-eval-worker.js +7 -1
  98. package/dist/src/duck/pg-eval.js +6 -4
  99. package/dist/src/duck/pgtree.js +774 -23
  100. package/dist/src/duck/plan.js +369 -47
  101. package/dist/src/duck/publication.js +20 -6
  102. package/dist/src/duck/requests.js +269 -93
  103. package/dist/src/duck/resolved.js +117 -2
  104. package/dist/src/duck/rewrite.js +4584 -580
  105. package/dist/src/duck/row-filter.js +655 -0
  106. package/dist/src/duck/statement.js +19 -0
  107. package/dist/src/duck/store.js +439 -177
  108. package/dist/src/duck/stream.js +19 -10
  109. package/dist/src/duck/sync.js +1411 -368
  110. package/dist/src/duck/token.js +4 -1
  111. package/dist/src/duck/types.js +69 -12
  112. package/dist/src/duck/usage.js +326 -0
  113. package/dist/src/duck/users.js +69 -15
  114. package/dist/src/duck/verified.js +161 -1
  115. package/dist/src/duck/views.js +5 -5
  116. package/dist/src/lib/database-name.js +62 -0
  117. package/dist/src/lib/database-url.js +55 -10
  118. package/dist/src/lib/dotenv.js +17 -4
  119. package/dist/src/lib/instant.js +50 -0
  120. package/dist/src/lib/log-buffer.js +77 -0
  121. package/dist/src/lib/log-read.js +196 -0
  122. package/dist/src/lib/pg-numeric.js +390 -49
  123. package/dist/src/lib/project-version.js +23 -23
  124. package/dist/src/lib/secret-mask.js +142 -0
  125. package/dist/src/lib/table-sync.js +282 -0
  126. package/dist/src/web/app.css +706 -66
  127. package/dist/src/web/app.js +331 -409
  128. package/dist/src/web/charts.js +229 -0
  129. package/dist/src/web/index.html +82 -18
  130. package/dist/src/web/pages/queries.js +192 -0
  131. package/dist/src/web/pages/status.js +611 -0
  132. package/dist/src/web/pages/syncs.js +155 -0
  133. package/dist/src/web/pages/tables.js +439 -0
  134. package/dist/src/web/pages/usage.js +397 -0
  135. package/dist/src/web/pages/users.js +219 -0
  136. package/dist/src/web/shared.js +584 -0
  137. package/dist/src/web/vendor/README.md +25 -0
  138. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  139. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  140. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  141. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  142. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  143. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  144. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  145. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  146. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  147. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  148. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  149. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  150. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  151. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  152. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  153. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  154. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  155. 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 { 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
- import { CANCEL_SLOTS, cancelTable, isCancelled, slotOf } from "./cancel.js";
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 = {}) {
@@ -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 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));
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 (${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")})`);
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", `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")})`);
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 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);
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 instanceof PgEvalError ? `mason[${e.code}]: ${e.message}` : `mason: ${e.message}`);
853
+ info.setError(raised(e));
598
854
  }
599
855
  },
600
856
  }));
601
857
  }
602
- const steps = (json, either) => JSON.parse(json)
603
- .map((s) => typeof s === "number" ? { index: s } : either ? { either: s } : { key: s });
604
- /** The libc order's two functions (src/duck/libc-order.ts), for the order the source was read with: the characters
605
- * ICU's accent and case levels read, and the key (a value holding one not covered refuses, by name). */
606
- export function registerLibcOrder(conn, order) {
607
- scalar(conn, "__mason_libc_kept", [VARCHAR], VARCHAR, ([s]) => libcKept(order, String(s)));
608
- scalar(conn, "__mason_libc_key", [VARCHAR, VARCHAR], VARCHAR, ([s, icu]) => libcKey(order, String(s), String(icu)));
609
- // jsonb's order under it (src/duck/jsonb-order.ts): the characters of each string ICU's levels read, then the key
610
- // from ICU's keys of them (src/duck/rewrite.ts jsonbKey hands them in)
611
- keyScalar(conn, JSONB_KEY_FUNCTIONS.libc.strings, [VARCHAR], LIST(VARCHAR), ([j]) => listValue(jsonbLibcKept(order, String(j))));
612
- keyScalar(conn, JSONB_KEY_FUNCTIONS.libc.key, [VARCHAR, LIST(VARCHAR)], BLOB, ([j, keys]) => new DuckDBBlobValue(jsonbKey(String(j), { kind: "libc", order, icuKeys: icuKeysOf(keys) })));
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. An interrupt does not end those tasks (they run on until the close),
642
- * so a statement that calls one of these functions is cancelled another way: its session's cell of the CANCEL TABLE
643
- * (src/duck/cancel.ts) says it is cancelled, and the statement's next call raises. DuckDB may hand that error back
644
- * while another task of the query still waits for a call, so the result is closed only once the session's calls have
645
- * stopped (src/duck/store.ts `cleanUp`; a deadlock under load). The table is shared memory: the listener's thread writes it
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 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";
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
- /** when each session's last call was answered (performance.now(), on this thread): what a session's runner reads to know
664
- * that no task of a statement still waits for a call before it closes the statement's result (src/duck/store.ts
665
- * `cleanUp`) */
666
- const lastCallAt = new Float64Array(CANCEL_SLOTS);
667
- export function msSinceCall(slot) { return performance.now() - (lastCallAt[slot] ?? 0); }
668
- /** the session's vector (the first argument, a constant): raises when the session's statement is cancelled */
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]) => 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
+ });
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
- 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
+ }
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 new PgEvalError(e.sqlstate, e.message);
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", `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")})`);
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(JSON.parse(String(program)), values);
1131
+ return evaluateText(parsedProgram(String(program)), values);
791
1132
  }
792
1133
  catch (e) {
793
1134
  if (e instanceof NumericError)
794
- throw new PgEvalError(e.sqlstate, e.message);
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));