@mason-data/cli 0.6.2 → 0.8.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 (119) hide show
  1. package/README.md +147 -69
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog.js +258 -14
  4. package/dist/src/api/postgres/encode.js +3 -1
  5. package/dist/src/api/postgres/explain.js +3 -2
  6. package/dist/src/api/postgres/protocol.js +39 -23
  7. package/dist/src/api/postgres/server.js +1290 -223
  8. package/dist/src/api/postgres/translate.js +81 -9
  9. package/dist/src/api/request-log.js +18 -5
  10. package/dist/src/cdc/pg-wire.js +144 -38
  11. package/dist/src/cdc/poll.js +5 -5
  12. package/dist/src/cli/background.js +55 -8
  13. package/dist/src/cli/cancel.js +75 -0
  14. package/dist/src/cli/command.js +5 -3
  15. package/dist/src/cli/commands.js +843 -272
  16. package/dist/src/cli/control.js +59 -0
  17. package/dist/src/cli/demo.js +158 -0
  18. package/dist/src/cli/dev.js +458 -125
  19. package/dist/src/cli/doctor.js +25 -12
  20. package/dist/src/cli/guides.js +103 -31
  21. package/dist/src/cli/init.js +159 -58
  22. package/dist/src/cli/machine.js +18 -12
  23. package/dist/src/cli/main.js +5 -5
  24. package/dist/src/cli/mcp.js +735 -0
  25. package/dist/src/cli/metrics.js +79 -0
  26. package/dist/src/cli/pg-tls.js +13 -3
  27. package/dist/src/cli/pg-url.js +4 -0
  28. package/dist/src/cli/pin.js +13 -12
  29. package/dist/src/cli/preflight.js +24 -14
  30. package/dist/src/cli/project.js +48 -7
  31. package/dist/src/cli/readiness.js +38 -4
  32. package/dist/src/cli/registry.js +57 -35
  33. package/dist/src/cli/replication-checks.js +7 -5
  34. package/dist/src/cli/replication-mode.js +105 -26
  35. package/dist/src/cli/request-filter.js +134 -0
  36. package/dist/src/cli/roles.js +584 -0
  37. package/dist/src/cli/run-server.js +498 -133
  38. package/dist/src/cli/runs.js +1 -1
  39. package/dist/src/cli/security.js +10 -8
  40. package/dist/src/cli/serve.js +147 -40
  41. package/dist/src/cli/share-run.js +286 -0
  42. package/dist/src/cli/share.js +286 -0
  43. package/dist/src/cli/source-edit.js +168 -0
  44. package/dist/src/cli/sources.js +290 -39
  45. package/dist/src/cli/supervise.js +12 -1
  46. package/dist/src/cli/sync.js +55 -25
  47. package/dist/src/cli/tunnel.js +200 -0
  48. package/dist/src/cli/ui.js +9 -6
  49. package/dist/src/cli/usage.js +128 -0
  50. package/dist/src/cli/user.js +496 -40
  51. package/dist/src/cli/version.js +9 -7
  52. package/dist/src/demo/seed.js +240 -0
  53. package/dist/src/demo/source.js +655 -0
  54. package/dist/src/duck/cancel.js +10 -1
  55. package/dist/src/duck/distinct-key.js +228 -0
  56. package/dist/src/duck/events.js +134 -0
  57. package/dist/src/duck/executor-worker.js +25 -11
  58. package/dist/src/duck/executor.js +107 -53
  59. package/dist/src/duck/explain.js +158 -29
  60. package/dist/src/duck/extensions.js +160 -0
  61. package/dist/src/duck/functions.js +73 -19
  62. package/dist/src/duck/gaps.js +25 -11
  63. package/dist/src/duck/grants.js +176 -0
  64. package/dist/src/duck/history.js +123 -0
  65. package/dist/src/duck/jsonb-order.js +175 -0
  66. package/dist/src/duck/jsonb.js +10 -8
  67. package/dist/src/duck/kept-route.js +82 -2
  68. package/dist/src/duck/kept.js +211 -44
  69. package/dist/src/duck/pgtree.js +40 -0
  70. package/dist/src/duck/plan.js +120 -30
  71. package/dist/src/duck/publication.js +6 -6
  72. package/dist/src/duck/requests.js +265 -93
  73. package/dist/src/duck/rewrite.js +1193 -73
  74. package/dist/src/duck/row-filter.js +649 -0
  75. package/dist/src/duck/store.js +271 -159
  76. package/dist/src/duck/stream.js +10 -5
  77. package/dist/src/duck/sync.js +907 -275
  78. package/dist/src/duck/types.js +6 -5
  79. package/dist/src/duck/usage.js +324 -0
  80. package/dist/src/duck/users.js +64 -15
  81. package/dist/src/duck/verified.js +9 -0
  82. package/dist/src/duck/views.js +2 -3
  83. package/dist/src/lib/database-name.js +62 -0
  84. package/dist/src/lib/dotenv.js +17 -4
  85. package/dist/src/lib/log-buffer.js +68 -0
  86. package/dist/src/lib/pg-numeric.js +5 -2
  87. package/dist/src/lib/project-version.js +23 -23
  88. package/dist/src/lib/table-sync.js +267 -0
  89. package/dist/src/lib/timezone.js +3 -2
  90. package/dist/src/web/app.css +706 -55
  91. package/dist/src/web/app.js +330 -258
  92. package/dist/src/web/charts.js +229 -0
  93. package/dist/src/web/index.html +84 -15
  94. package/dist/src/web/pages/queries.js +192 -0
  95. package/dist/src/web/pages/status.js +446 -0
  96. package/dist/src/web/pages/syncs.js +155 -0
  97. package/dist/src/web/pages/tables.js +434 -0
  98. package/dist/src/web/pages/usage.js +397 -0
  99. package/dist/src/web/pages/users.js +218 -0
  100. package/dist/src/web/shared.js +568 -0
  101. package/dist/src/web/vendor/README.md +25 -0
  102. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  103. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  104. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  105. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  106. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  107. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  108. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  109. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  110. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  111. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  112. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  113. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  114. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  115. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  116. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  117. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  118. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  119. package/package.json +10 -3
@@ -0,0 +1,160 @@
1
+ /**
2
+ * THE COPY'S EXTENSIONS SHIP WITH MASON. The copy reads the source through DuckDB's `postgres` extension
3
+ * (src/duck/sync.ts attach), and a statement that casts a value to inet needs DuckDB's `inet`; DuckDB bundles
4
+ * neither, and by default downloads one from its extension repository at the first statement that needs it. Mason
5
+ * ships both instead, ONE npm PACKAGE PER PLATFORM (`@mason-data/extensions-<platform>`), as DuckDB's node bindings ship
6
+ * their binary: an optional dependency of @mason-data/cli whose `os` / `cpu` / `libc` make npm install the one this
7
+ * machine runs, built by cli/extensions.ts from the files' checksums pinned in cli/extensions.json. The image installs
8
+ * the same package (docker/mason-data.Dockerfile).
9
+ *
10
+ * - A package found (`findExtensions`): the instance's `extension_directory` IS the package's folder and nothing is
11
+ * ever installed (`extensionSettings`): `LOAD postgres` reads it, an inet cast autoloads it from there, and
12
+ * autoinstall is off, so no statement downloads anything. DuckDB checks each file's signature at LOAD
13
+ * (`allow_unsigned_extensions` stays off: the official extensions are signed), and LOAD writes nothing, so the
14
+ * folder may be read-only (a global npm install, the image's /app).
15
+ * - None (`npm install --omit=optional`, a platform no package is built for — musl, Windows on arm64 — or a checkout):
16
+ * DuckDB's own way, said with the reason: the first copy installs the extension from the internet into
17
+ * ~/.duckdb/extensions (src/duck/sync.ts attach). `mason doctor` says which (`probeExtensions`).
18
+ *
19
+ * tests/duck-extensions.test.ts holds the resolver per platform over a fake package; the integration test
20
+ * (tests/integration/extensions.itest.ts) holds a copy and an inet cast to the package with nothing downloaded.
21
+ */
22
+ import * as fs from "node:fs";
23
+ import * as path from "node:path";
24
+ import { createRequire } from "node:module";
25
+ import { DuckDBInstance, version } from "@duckdb/node-api";
26
+ /** Each platform a package is built for: Node's `<platform>-<arch>` (the package's name) and DuckDB's name for it (the
27
+ * folder of the extension repository, and of `extension_directory`). Linux is glibc's: musl has no package. */
28
+ export const EXTENSION_PLATFORMS = {
29
+ "darwin-arm64": "osx_arm64",
30
+ "darwin-x64": "osx_amd64",
31
+ "linux-arm64": "linux_arm64",
32
+ "linux-x64": "linux_amd64",
33
+ "win32-x64": "windows_amd64",
34
+ };
35
+ /** The extensions each package holds, by the name DuckDB installs them under (`INSTALL postgres` is `postgres_scanner`). */
36
+ export const EXTENSION_NAMES = ["postgres_scanner", "inet"];
37
+ export const packageName = (p) => `@mason-data/extensions-${p}`;
38
+ /** Where a package keeps the files: `<package>/extensions/<engine>/<DuckDB's platform>/<name>.duckdb_extension`, the
39
+ * layout DuckDB reads `extension_directory` in. */
40
+ export const extensionFile = (root, engine, platform, name) => path.join(root, "extensions", engine, EXTENSION_PLATFORMS[platform], `${name}.duckdb_extension`);
41
+ export function thisHost() {
42
+ // Node's own report names glibc's version where glibc runs; musl (Alpine) has none (detect-libc's first check). Its
43
+ // network part is left out, as detect-libc leaves it: enumerating the interfaces has taken seconds in a container
44
+ let header;
45
+ if (process.platform === "linux") {
46
+ // `excludeNetwork` is Node's (20+), not yet in @types/node
47
+ const report = process.report;
48
+ const network = report.excludeNetwork;
49
+ report.excludeNetwork = true;
50
+ try {
51
+ header = report.getReport().header;
52
+ }
53
+ finally {
54
+ report.excludeNetwork = network;
55
+ }
56
+ }
57
+ return { platform: process.platform, arch: process.arch, musl: process.platform === "linux" && header?.glibcVersionRuntime === undefined };
58
+ }
59
+ /** The platform's key as npm and DuckDB's bindings name it: `linux-x64`, `linux-x64-musl`, `darwin-arm64`. */
60
+ export const hostKey = (h) => `${h.platform}-${h.arch}${h.musl ? "-musl" : ""}`;
61
+ const requireHere = createRequire(import.meta.url);
62
+ /**
63
+ * THE PACKAGE FOR THIS MACHINE, found where Node would find it from this module (the CLI's node_modules, nested or
64
+ * hoisted; the image's /app/node_modules), holding both files for THIS engine. Reads two files' existence: no engine
65
+ * is opened. `resolve` and `host` are a test's.
66
+ */
67
+ export function findExtensions(o = {}) {
68
+ const host = o.host ?? thisHost();
69
+ const engine = o.engine ?? `v${version().replace(/^v/, "")}`;
70
+ const key = hostKey(host);
71
+ if (!(key in EXTENSION_PLATFORMS)) {
72
+ return { shipped: false, package: null, why: `no package of Mason's extensions is built for ${key} (only ${Object.keys(EXTENSION_PLATFORMS).join(", ")})` };
73
+ }
74
+ const platform = key;
75
+ const name = packageName(platform);
76
+ let manifest;
77
+ try {
78
+ manifest = (o.resolve ?? requireHere.resolve)(`${name}/package.json`);
79
+ }
80
+ catch (e) {
81
+ // not installed is the one answer expected here; anything else is said as it is
82
+ if (e.code !== "MODULE_NOT_FOUND")
83
+ throw e;
84
+ return { shipped: false, package: name, why: `${name} is not installed beside Mason (npm installs it with @mason-data/cli unless optional dependencies are left out: --omit=optional)` };
85
+ }
86
+ const root = path.dirname(manifest);
87
+ let pkgVersion;
88
+ try {
89
+ pkgVersion = String(JSON.parse(fs.readFileSync(manifest, "utf8")).version ?? "");
90
+ }
91
+ catch (e) {
92
+ // a broken install: refused in Mason's words with the way out, never the parser's error
93
+ return { shipped: false, package: name, why: `${name}'s package.json at ${manifest} cannot be read (${e.message}): install @mason-data/cli again` };
94
+ }
95
+ const missing = EXTENSION_NAMES.filter((n) => !fs.existsSync(extensionFile(root, engine, platform, n)));
96
+ if (missing.length > 0) {
97
+ const has = fs.existsSync(path.join(root, "extensions")) ? fs.readdirSync(path.join(root, "extensions")) : [];
98
+ return { shipped: false, package: name, why: `${name} ${pkgVersion} holds no ${missing.join(" or ")} for the engine Mason runs (${engine}; it holds ${has.length ? has.join(", ") : "none"}): ` +
99
+ "install @mason-data/cli again, whose packages are released together" };
100
+ }
101
+ return { shipped: true, package: name, version: pkgVersion, directory: path.join(root, "extensions"), platform };
102
+ }
103
+ /** The instance's settings for what was found: with the package, its folder and no download, ever; without, DuckDB's
104
+ * defaults (the first copy installs into ~/.duckdb/extensions). */
105
+ export function extensionSettings(x) {
106
+ return x.shipped ? { extension_directory: x.directory, autoinstall_known_extensions: "false", autoload_known_extensions: "true" } : {};
107
+ }
108
+ /** The statement the copy loads its postgres extension with: from the package, a LOAD; without one, DuckDB's INSTALL
109
+ * first (a download, once a machine). */
110
+ export const loadPostgresSql = (x) => x.shipped ? "LOAD postgres" : "INSTALL postgres; LOAD postgres;";
111
+ /** What a copy that could not load the postgres extension says (src/duck/sync.ts attach), with the way out. */
112
+ export function extensionFailure(x, error) {
113
+ return x.shipped
114
+ ? `Mason could not load its postgres extension from ${x.package} ${x.version} (${x.directory}): ${error}. Install @mason-data/cli again; mason doctor checks it`
115
+ : `Mason could not load its postgres extension: ${error}. ${x.why}, so the first sync on a machine downloads it ` +
116
+ "(then ~/.duckdb/extensions): connect to the internet once, and the sync is tried again at the next pass";
117
+ }
118
+ /**
119
+ * THE PROBE `mason doctor` SAYS (the contract answers a probe, never a file's existence): with the package, both
120
+ * extensions LOADED by an instance of their own with the run's settings — the signature and the platform checked as
121
+ * the run's will be; without one, whether DuckDB has them installed in ~/.duckdb/extensions already, and `ok` null when
122
+ * it has not (the first copy will download them: fine online, which no probe can tell). Downloads nothing.
123
+ */
124
+ export async function probeExtensions(x = findExtensions()) {
125
+ let instance;
126
+ try {
127
+ instance = await DuckDBInstance.create(":memory:", { autoinstall_known_extensions: "false", ...extensionSettings(x) });
128
+ }
129
+ catch (e) {
130
+ return { ok: null, detail: `could not open an engine to check them: ${e.message}` };
131
+ }
132
+ try {
133
+ const c = await instance.connect();
134
+ try {
135
+ if (x.shipped) {
136
+ for (const n of EXTENSION_NAMES) {
137
+ try {
138
+ await c.run(`LOAD ${n}`);
139
+ }
140
+ catch (e) {
141
+ return { ok: false, detail: `${x.package} ${x.version} is installed, and its ${n.replace("_scanner", "")} extension does not load: ${e.message.split("\n")[0]}. Install @mason-data/cli again` };
142
+ }
143
+ }
144
+ return { ok: true, detail: `postgres and inet ship with Mason (${x.package} ${x.version}): nothing is downloaded` };
145
+ }
146
+ const rows = (await c.runAndReadAll(`SELECT extension_name, installed FROM duckdb_extensions() WHERE extension_name IN (${EXTENSION_NAMES.map((n) => `'${n}'`).join(", ")})`)).getRowObjects();
147
+ const absent = EXTENSION_NAMES.filter((n) => !rows.some((r) => r.extension_name === n && r.installed === true)).map((n) => n.replace("_scanner", ""));
148
+ if (absent.length === 0)
149
+ return { ok: true, detail: `postgres and inet were downloaded on this machine before (~/.duckdb/extensions), not shipped: ${x.why}` };
150
+ // not a failure: online, the first copy downloads them; whether this machine will be online then, no probe can tell
151
+ return { ok: null, detail: `${absent.join(" and ")} not on this machine yet, so the first sync downloads them and needs the internet once: ${x.why}` };
152
+ }
153
+ finally {
154
+ c.closeSync();
155
+ }
156
+ }
157
+ finally {
158
+ instance.closeSync();
159
+ }
160
+ }
@@ -10,18 +10,20 @@
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, BOOLEAN, DATE, DOUBLE, DuckDBDateValue, DuckDBIntervalValue, DuckDBScalarFunction, DuckDBTimestampTZValue, DuckDBTimestampValue, INTEGER, INTERVAL, LIST, TIMESTAMP, TIMESTAMPTZ, VARCHAR, listValue } from "@duckdb/node-api";
13
+ import { BIGINT, BLOB, BOOLEAN, DATE, DOUBLE, DuckDBBlobValue, DuckDBDateValue, DuckDBIntervalValue, DuckDBScalarFunction, DuckDBTimestampTZValue, DuckDBTimestampValue, INTEGER, INTERVAL, LIST, TIMESTAMP, TIMESTAMPTZ, VARCHAR, listValue } from "@duckdb/node-api";
14
14
  import { pgEvalSync, PgEvalError } from "./pg-eval.js";
15
+ import { MARKED_FUNCTIONS } from "./kept-route.js";
15
16
  import { libcKept, libcKey } from "./libc-order.js";
16
17
  import { formatFloat } from "../api/postgres/encode.js";
17
18
  import { floatAsNumeric } from "./types.js";
18
19
  import { intervalText } from "./types.js";
19
20
  import { jsonbText, parseJson, writeJson, quoteJson } from "./jsonb.js";
21
+ import { JSONB_KEY_FUNCTIONS, jsonbKey, jsonbLibcKept, jsonbStrings, STORED_SUFFIX } from "./jsonb-order.js";
20
22
  import { compare, div, evaluateText, NumericError, numericText, parseNumeric } from "../lib/pg-numeric.js";
21
23
  import { toCharNumber, toCharTime } from "./to-char.js";
22
24
  import { atTimeZoneOf } from "../lib/timezone.js";
23
25
  import { gapMark } from "./gaps.js";
24
- import { CANCEL_SLOTS, cancelTable, isCancelled, slotOf } from "./cancel.js";
26
+ import { beat, cancelTable, isCancelled, slotOf } from "./cancel.js";
25
27
  // ------------------------------------------------------------------------------------------------- case mapping
26
28
  /** one character's simple mapping, as towupper / towlower give it: a character whose full mapping is several (`ß`,
27
29
  * `ʼn`) keeps itself */
@@ -445,7 +447,12 @@ function contains(a, b) {
445
447
  }
446
448
  /** `a @> b` on jsonb (jsonb_contains): their roots must both be objects or both not, then JsonbDeepContains */
447
449
  export function jsonbContains(a, b) {
448
- const x = root(parseJson(a)), y = root(parseJson(b));
450
+ const pa = parseJson(a), pb = parseJson(b);
451
+ const x = root(pa), y = root(pb);
452
+ // a raw scalar (stored as a one-element array) contains no array: `'1' @> '[1]'` is false, `'[1]' @> '1'` true
453
+ // (JsonbDeepContains refuses a raw scalar on the left of a real array)
454
+ if (pa.t !== "arr" && pa.t !== "obj" && pb.t === "arr")
455
+ return false;
449
456
  return (x.t === "obj") === (y.t === "obj") && contains(x, y);
450
457
  }
451
458
  /** `j ? key` on jsonb (jsonb_exists): an object's top-level key, or a string element of an array (a string scalar's own) */
@@ -482,12 +489,14 @@ function scalar(conn, name, params, returns, body, opts = {}) {
482
489
  specialHandling: true,
483
490
  mainFunction: (info, input, out) => {
484
491
  FUNCTION_CALLS.set(name, (FUNCTION_CALLS.get(name) ?? 0) + 1);
492
+ FUNCTION_ROWS.set(name, (FUNCTION_ROWS.get(name) ?? 0) + input.rowCount);
485
493
  const all = Array.from({ length: input.columnCount }, (_, c) => input.getColumnVector(c));
486
494
  if (cancelled(info, all, input.rowCount))
487
495
  return;
488
496
  const vectors = all.slice(1);
489
497
  try {
490
498
  for (let r = 0; r < input.rowCount; r++) {
499
+ beat(cancels); // one vector of wide documents is seconds of this loop: the watch must see it run
491
500
  const args = vectors.map((v) => v.getItem(r));
492
501
  out.setItem(r, opts.nulls !== "pass" && args.some((a) => a === null) ? null : body(args));
493
502
  }
@@ -598,40 +607,68 @@ const steps = (json, either) => JSON.parse(json)
598
607
  export function registerLibcOrder(conn, order) {
599
608
  scalar(conn, "__mason_libc_kept", [VARCHAR], VARCHAR, ([s]) => libcKept(order, String(s)));
600
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) })));
614
+ }
615
+ /**
616
+ * A FUNCTION OF A JSONB'S KEY (src/duck/jsonb-order.ts `JSONB_KEY_FUNCTIONS`), and its stored twin (`STORED_SUFFIX`),
617
+ * which a write of the copy computes a stored key with (src/duck/kept.ts `orderKeySql`): the same body, answering NULL
618
+ * where it raises — a write of the copy (the stream's, the poll's) must not fail over one row's document. The stored
619
+ * key's SQL marks that row to be computed when read (src/duck/kept-route.ts `KEY_RECOMPUTE_HEX`), so the read raises its own error
620
+ * (a libc string the order does not cover: `libc-uncovered`), as the statement would without the stored key.
621
+ */
622
+ function keyScalar(conn, name, params, returns, body) {
623
+ scalar(conn, name, params, returns, body);
624
+ scalar(conn, `${name}${STORED_SUFFIX}`, params, returns, (args) => {
625
+ try {
626
+ return body(args);
627
+ }
628
+ catch {
629
+ return null;
630
+ } // the row's mark: its read computes it, and raises (above)
631
+ });
632
+ }
633
+ /** the ICU keys DuckDB made of a jsonb's strings (a NULL among them is a Mason bug: every string has one) */
634
+ function icuKeysOf(v) {
635
+ return itemsOf(v).map((k) => { if (k === null)
636
+ throw new Error("an ICU key of a jsonb's string is NULL: a Mason bug"); return k; });
601
637
  }
602
638
  /**
603
639
  * EVERY CALL CARRIES ITS SESSION, FIRST (the test server: a client leaving mid-statement hung the process
604
640
  * for good). A scalar function runs on the thread that registered it — the executor worker (src/duck/executor.ts) —
605
641
  * while an executor thread waits for it, so that thread can never wait for the executor — and closing a result or a
606
- * connection does, to cancel the executor's tasks. An interrupt does not end those tasks (they run on until the close),
607
- * so a statement that calls one of these functions is cancelled another way: its session's cell of the CANCEL TABLE
608
- * (src/duck/cancel.ts) says it is cancelled, and the statement's next call raises. DuckDB may hand that error back
609
- * while another task of the query still waits for a call, so the result is closed only once the session's calls have
610
- * stopped (src/duck/store.ts `cleanUp`; a deadlock under load). The table is shared memory: the listener's thread writes it
642
+ * connection does, to cancel the executor's tasks. So such a statement is cancelled through its session as well as
643
+ * interrupted: its session's cell of the CANCEL TABLE (src/duck/cancel.ts) says it is cancelled, the statement's next call
644
+ * raises, and its result is closed only once DuckDB ended the query off this thread (src/duck/store.ts `cleanUp`; a
645
+ * deadlock under load). The table is shared memory: the listener's thread writes it
611
646
  * while this one is busy with the statement's calls. The rewrite (src/duck/rewrite.ts `fn`) passes
612
647
  * `getvariable('__mason_session')` as every `__mason_*` function's first argument, which DuckDB folds to the session's
613
648
  * literal at bind time; the session runner sets the variable on its connection (its slot, `s<n>`). A connection without
614
649
  * it (the writer, a copy's) passes NULL: never cancelled this way.
615
650
  */
616
651
  export const SESSION_VARIABLE = "__mason_session";
652
+ /** the session's settings, name to value, as `current_setting(name)` answers them (src/api/postgres/server.ts
653
+ * `duckSession`): set on a session's connection before each statement that finds them changed (src/duck/store.ts
654
+ * `settle`), read by the rewrite's `settingRead` (src/duck/rewrite.ts) */
655
+ export const SETTINGS_VARIABLE = "__mason_settings";
617
656
  export const CANCELLED = "mason[57014]: canceling statement due to user request";
618
657
  /** how many vectors each function was called with (a call is a hop to the main thread): what a test or a measurement
619
658
  * reads to know whether a statement reached a function at all (the rewrite answers a `->>` natively where it can) */
620
659
  export const FUNCTION_CALLS = new Map();
660
+ /** how many rows each function was handed, across its calls: what a plan's cost in function calls is measured by,
661
+ * whatever the chunking (tests/duck-plans.test.ts) */
662
+ export const FUNCTION_ROWS = new Map();
621
663
  /** the cancel table the calls read: the executor's, shared with the listener (`useCancelTable`), else one of this thread's own */
622
664
  let cancels = cancelTable();
623
665
  export function useCancelTable(t) { cancels = t; }
624
666
  export function cancelsTable() { return cancels; }
625
- /** when each session's last call was answered (performance.now(), on this thread): what a session's runner reads to know
626
- * that no task of a statement still waits for a call before it closes the statement's result (src/duck/store.ts
627
- * `cleanUp`) */
628
- const lastCallAt = new Float64Array(CANCEL_SLOTS);
629
- export function msSinceCall(slot) { return performance.now() - (lastCallAt[slot] ?? 0); }
630
- /** the session's vector (the first argument, a constant): raises when the session's statement is cancelled */
667
+ /** the session's vector (the first argument, a constant): raises when the session's statement is cancelled. Every call
668
+ * starts here, so every call beats (src/duck/cancel.ts) */
631
669
  function cancelled(info, vectors, rows) {
670
+ beat(cancels);
632
671
  const slot = rows > 0 ? slotOf(text(vectors[0]?.getItem(0))) : null;
633
- if (slot !== null)
634
- lastCallAt[slot] = performance.now();
635
672
  if (slot === null || !isCancelled(cancels, slot))
636
673
  return false;
637
674
  info.setError(CANCELLED);
@@ -647,10 +684,27 @@ export function registerFunctions(conn) {
647
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));
648
685
  // the same, as a stored JSON path keeps it (keptJsonValue, src/duck/kept.ts): what the copy's writes compute
649
686
  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));
650
- scalar(conn, "__mason_json_elements", [VARCHAR, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, asText]) => listValue(jsonElements(String(j), canonical === true, asText === true)));
651
- scalar(conn, "__mason_json_keys", [VARCHAR, BOOLEAN], LIST(VARCHAR), ([j, canonical]) => listValue(jsonMembers(String(j), canonical === true, false).map(([k]) => k)));
652
- scalar(conn, "__mason_json_values", [VARCHAR, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, asText]) => listValue(jsonMembers(String(j), canonical === true, asText === true).map(([, v]) => v)));
687
+ // a document's rows as a list each (elements, keys, values); `orNull` (the last argument): never empty, a NULL or
688
+ // empty document's list one NULL — a LEFT JOIN LATERAL … ON TRUE's one row of NULLs, as an INNER lateral DuckDB runs
689
+ // in its rows' order and stops at a LIMIT (src/duck/rewrite.ts `leftLateral`)
690
+ const rows = (j, orNull, list) => {
691
+ const items = j === null || j === undefined ? null : list(String(j));
692
+ return orNull === true && (items === null || items.length === 0) ? listValue([null]) : items === null ? null : listValue(items);
693
+ };
694
+ scalar(conn, "__mason_json_elements", [VARCHAR, BOOLEAN, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, asText, orNull]) => rows(j, orNull, (t) => jsonElements(t, canonical === true, asText === true)), { nulls: "pass" });
695
+ scalar(conn, "__mason_json_keys", [VARCHAR, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, orNull]) => rows(j, orNull, (t) => jsonMembers(t, canonical === true, false).map(([k]) => k)), { nulls: "pass" });
696
+ scalar(conn, "__mason_json_values", [VARCHAR, BOOLEAN, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, asText, orNull]) => rows(j, orNull, (t) => jsonMembers(t, canonical === true, asText === true).map(([, v]) => v)), { nulls: "pass" });
653
697
  scalar(conn, "__mason_jsonb_text", [VARCHAR], VARCHAR, ([j]) => jsonbText(String(j)));
698
+ // jsonb's order and equality as bytes (src/duck/jsonb-order.ts): under C, and under ICU from ICU's keys of the
699
+ // document's strings, which the rewrite has DuckDB make of `__mason_jsonb_strings` (src/duck/rewrite.ts jsonbKey)
700
+ keyScalar(conn, JSONB_KEY_FUNCTIONS.bytes.key, [VARCHAR], BLOB, ([j]) => new DuckDBBlobValue(jsonbKey(String(j), { kind: "bytes" })));
701
+ // a stored column's marked row read exact raises its error (src/duck/kept-route.ts markedError): never DuckDB's
702
+ // volatile error(), which a window's ORDER BY refuses
703
+ const marked = ([, code, message]) => { throw new PgEvalError(String(code), String(message)); };
704
+ scalar(conn, MARKED_FUNCTIONS.key, [VARCHAR, VARCHAR, VARCHAR], BLOB, marked);
705
+ scalar(conn, MARKED_FUNCTIONS.value, [VARCHAR, VARCHAR, VARCHAR], VARCHAR, marked);
706
+ keyScalar(conn, JSONB_KEY_FUNCTIONS.icu.strings, [VARCHAR], LIST(VARCHAR), ([j]) => listValue(jsonbStrings(String(j))));
707
+ keyScalar(conn, JSONB_KEY_FUNCTIONS.icu.key, [VARCHAR, LIST(VARCHAR)], BLOB, ([j, keys]) => new DuckDBBlobValue(jsonbKey(String(j), { kind: "icu", icuKeys: icuKeysOf(keys) })));
654
708
  // jsonb's @>, ?, ?| and ?& (DuckDB's json_contains and json_exists answer otherwise on arrays), json(b)_array_length
655
709
  scalar(conn, "__mason_jsonb_contains", [VARCHAR, VARCHAR], BOOLEAN, ([a, b]) => jsonbContains(String(a), String(b)));
656
710
  scalar(conn, "__mason_jsonb_exists", [VARCHAR, VARCHAR], BOOLEAN, ([j, k]) => jsonbExists(String(j), String(k)));
@@ -17,11 +17,12 @@
17
17
  */
18
18
  export const GAPS = {
19
19
  // ---------------------------------------------------------------------------------------------- refused
20
- unverified: { kind: "refused", what: "a function, operator, cast, type or construct the oracle has not held to Postgres", postgres: "answers", mason: "refused, naming each piece", wayOut: "write the statement without it; report it to have it verified", heldBy: "tests/duck-wire.test.ts" },
20
+ unverified: { kind: "refused", what: "a function, operator, cast, type or construct Mason has not yet verified against Postgres", postgres: "answers", mason: "refused, naming each piece", wayOut: "write the statement without it; report it to have it verified", heldBy: "tests/duck-wire.test.ts" },
21
21
  "explain-option": { kind: "refused", what: "EXPLAIN (FORMAT XML), (FORMAT YAML), (GENERIC_PLAN)", postgres: "the plan in XML or YAML; a plan for unbound parameters", mason: "refused by name: EXPLAIN is DuckDB's plan, in Postgres's text or JSON (docs/DESIGN.md)", wayOut: "FORMAT TEXT or FORMAT JSON; bind the parameters' values and EXPLAIN that" },
22
+ "set-config-forms": { kind: "refused", what: "set_config() anywhere but alone in a SELECT of no table with literal arguments: beside other columns or a FROM, in a subquery, with an expression, a parameter or NULL, under EXPLAIN", postgres: "sets the parameter (for the transaction, under is_local) and answers its value", mason: "refused (SELECT set_config('name', 'value', false) alone is the session's SET, and with true its SET LOCAL, answered with its value as SHOW answers it)", wayOut: "SET name = 'value', or SELECT set_config('name', 'value', false) on its own" },
22
23
  "setop-ordinal-star": { kind: "refused", what: "ORDER BY a column number of a UNION / INTERSECT / EXCEPT whose first SELECT lists *", postgres: "sorts by that column", mason: "refused", wayOut: "ORDER BY the column's name" },
23
24
  "with-ties-distinct": { kind: "refused", what: "FETCH FIRST … WITH TIES over SELECT DISTINCT", postgres: "answers", mason: "refused (DuckDB has no WITH TIES; Mason's rank over DISTINCT is not Postgres's)", wayOut: "select the DISTINCT rows in a subquery and FETCH … WITH TIES from it" },
24
- "collation-unknown": { kind: "refused", what: "COLLATE with a collation other than C, POSIX, default or an ICU locale DuckDB has", postgres: "sorts and compares under it", mason: "refused", wayOut: "COLLATE \"C\" for byte order, or no COLLATE for the database's" },
25
+ "collation-unknown": { kind: "refused", what: "COLLATE with a collation other than C, POSIX, default or an ICU locale the engine has", postgres: "sorts and compares under it", mason: "refused", wayOut: "COLLATE \"C\" for byte order, or no COLLATE for the database's" },
25
26
  "like-any-array": { kind: "refused", what: "LIKE / ILIKE ANY (or ALL) over an array that is not written out", postgres: "answers", mason: "refused", wayOut: "write the patterns as ARRAY['…', '…'], or OR the LIKEs" },
26
27
  "similar-to-forms": { kind: "refused", what: "SIMILAR TO with a pattern or ESCAPE that is not a literal, its substring markers, or substring(… SIMILAR … ESCAPE …)", postgres: "answers", mason: "refused", wayOut: "a literal pattern, or ~ with a POSIX regular expression" },
27
28
  "array-remove-type": { kind: "refused", what: "array_remove of an array of a type other than text, integers, floats, booleans, dates or timestamps", postgres: "answers", mason: "refused", wayOut: "array_remove an array of one of those types" },
@@ -33,11 +34,12 @@ export const GAPS = {
33
34
  "json-of-type": { kind: "refused", what: "to_json / array_to_json / json_build_object of a value (or an array of elements) of a type Mason does not write as json", postgres: "answers", mason: "refused", wayOut: "cast the value to text, a number, a boolean, a timestamp or json first" },
34
35
  "extract-julian": { kind: "refused", what: "extract(julian FROM …)", postgres: "answers", mason: "refused", wayOut: "extract(epoch FROM …) / 86400 + 2440587.5" },
35
36
  "with-ordinality": { kind: "refused", what: "a function in FROM … WITH ORDINALITY", postgres: "numbers the rows", mason: "refused", wayOut: "row_number() OVER () instead" },
37
+ "array-subquery-element": { kind: "refused", what: "ARRAY(SELECT x …) whose x is a numeric, an array or a row; x = ANY or ALL (ARRAY(SELECT …))", postgres: "a numeric[] computed exactly, a multi-dimensional array (2202E on an empty element, 22004 on a NULL one), a record[]", mason: "refused (DuckDB makes a list of doubles, of lists, of structs; it binds ANY (ARRAY(SELECT …)) as a comparison of its own)", wayOut: "ARRAY(SELECT x::text …), array_agg(x ORDER BY …) in a subquery, or x = ANY (SELECT …) without the ARRAY" },
36
38
  "multidim-array": { kind: "refused", what: "a multi-dimensional array literal", postgres: "answers", mason: "refused", wayOut: "a one-dimensional array" },
37
39
  "format-specifiers": { kind: "refused", what: "format() with a specifier other than %s (%I, %L, widths, positions)", postgres: "answers", mason: "refused", wayOut: "concatenate with ||, or %s only" },
38
40
  "to-char-patterns": { kind: "refused", what: "to_char of an interval or a time; its TZ, SP, EEEE, RN, V, B, L patterns; a day or month name padded to 9 characters in a parse", postgres: "answers", mason: "refused", wayOut: "the patterns listed in the hint; extract(… FROM interval) for an interval's parts" },
39
41
  "to-timestamp-format": { kind: "refused", what: "to_timestamp / to_date with a format that is not a literal", postgres: "answers", mason: "refused", wayOut: "write the format as a string literal" },
40
- "libc-uncovered": { kind: "refused", what: "ORDER BY, <, >, min / max of text holding a combining mark (e + U+0301), a Thai or Lao vowel written before its consonant, a private-use or unassigned code point, under a libc collation", postgres: "answers, in glibc's order", mason: "refused when a value holding one is ordered (the order is read from the database, and glibc orders these otherwise than Mason can follow)", wayOut: "normalize the text (normalize(x, NFC)), or ORDER BY x COLLATE \"C\" for byte order" },
42
+ "libc-uncovered": { kind: "refused", what: "ORDER BY, <, >, min / max of text (or of a jsonb, by its strings) holding a combining mark (e + U+0301), a Thai or Lao vowel written before its consonant, a private-use or unassigned code point, under a libc collation", postgres: "answers, in glibc's order", mason: "refused when a value holding one is ordered (the order is read from the database, and glibc orders these otherwise than Mason can follow)", wayOut: "normalize the text (normalize(x, NFC)), or ORDER BY x COLLATE \"C\" for byte order" },
41
43
  "libc-symbol-punctuation": { kind: "differs", what: "text ordered under a libc collation where punctuation or a space follows a digit or a symbol and a later letter differs only in case ('1-X' beside '1x')", postgres: "the punctuation outranks the case: 1-X < 1x", mason: "the case outranks the punctuation: 1x < 1-X", wayOut: "ORDER BY lower(x), x when the case must not decide first" },
42
44
  "percentile-text-libc": { kind: "refused", what: "percentile_disc / mode() WITHIN GROUP (ORDER BY text) under a libc collation", postgres: "answers", mason: "refused", wayOut: "order by a number, or ORDER BY … LIMIT in a subquery" },
43
45
  "infinite-extract": { kind: "refused", what: "extract(year, decade, century, millennium, isoyear or epoch FROM) an infinite date or timestamp", postgres: "Infinity (a numeric)", mason: "refused when a row holds one (DuckDB's DECIMAL has no Infinity)", wayOut: "filter the infinite values out (isfinite(x)), or date_part(…), which is a double" },
@@ -45,7 +47,7 @@ export const GAPS = {
45
47
  "quote-ident": { kind: "refused", what: "quote_ident()", postgres: "quotes an identifier when it must be", mason: "refused (it needs Postgres's keyword list)", wayOut: "quote it in the statement, or format('%s', …)" },
46
48
  "div-numeric": { kind: "refused", what: "div() of a value that is not an integer", postgres: "the integer quotient of two numerics", mason: "refused", wayOut: "trunc(a / b)" },
47
49
  "jsonb-set": { kind: "refused", what: "jsonb_set, jsonb_insert, the #- operator, jsonb - of a text[] or an index", postgres: "answers", mason: "refused", wayOut: "build the value with jsonb_build_object" },
48
- "jsonb-compare": { kind: "refused", what: "ORDER BY, GROUP BY, DISTINCT or <, >, = over jsonb (Postgres orders and equates jsonb by its own rules: numbers by value, objects by size first)", postgres: "answers", mason: "refused", wayOut: "compare a member: j ->> 'k', or j::text for the canonical text" },
50
+ "jsonb-compare": { kind: "refused", what: "a jsonb compared by GREATEST / LEAST, WITHIN GROUP (ORDER BY …), IN or = ANY of a subquery or of an array not written out, BETWEEN SYMMETRIC, JOIN … USING or NATURAL JOIN, UNION / INTERSECT / EXCEPT (but UNION ALL), SELECT DISTINCT *, GROUPING SETS / ROLLUP / CUBE, or an aggregate's DISTINCT but count's; ORDER BY the number of a jsonb column a * lists; a row holding a jsonb compared, grouped or sorted; a jsonb[] sorted, grouped, compared, contained (@>, <@, &&) or searched (array_position, array_remove)", postgres: "answers, by jsonb's own order and equality", mason: "refused (ORDER BY, GROUP BY, DISTINCT [ON], =, <, >, IN (…), = ANY (ARRAY[…]), BETWEEN, IS DISTINCT FROM, NULLIF, CASE x WHEN, PARTITION BY and count(DISTINCT …) of a jsonb are answered in its order)", wayOut: "compare the jsonb in one of the answered forms, a member (j ->> 'k'), or j::text for the canonical text" },
49
51
  "text-array-order": { kind: "refused", what: "ORDER BY, GROUP BY, DISTINCT, min, max or <, > over a text[] under the database's collation", postgres: "compares the elements under the collation", mason: "refused", wayOut: "sort by an element (a[1]), or by array_to_string(a, ',')" },
50
52
  "sum-real": { kind: "refused", what: "sum() of a real (float4), as an aggregate or a window", postgres: "accumulates in single precision", mason: "refused (DuckDB accumulates in double precision)", wayOut: "sum(x::double precision)" },
51
53
  "interval-aggregates": { kind: "refused", what: "sum() or avg() of an interval", postgres: "adds months, days and time apart", mason: "refused", wayOut: "sum(extract(epoch FROM x)) for the seconds" },
@@ -54,7 +56,7 @@ export const GAPS = {
54
56
  "inet-order": { kind: "refused", what: "ORDER BY, <, >, min or max over an inet", postgres: "orders addresses by network, then host", mason: "refused (equality, IS NULL and the value itself are answered)", wayOut: "sort or compare another column" },
55
57
  "citext-forms": { kind: "refused", what: "GROUP BY, DISTINCT, min, max, a set operation, or = ANY (an array) over a citext", postgres: "groups and sorts without regard to case, answering one spelling", mason: "refused (=, <>, <, >, IN (…), LIKE and ORDER BY are answered, case-insensitively)", wayOut: "GROUP BY lower(x), or x::text where case matters" },
56
58
  "bytea-forms": { kind: "refused", what: "encode / decode to a format other than hex, base64 or escape, or one not written as a literal; convert_to / convert_from of an encoding other than UTF8", postgres: "answers", mason: "refused", wayOut: "encode(x, 'hex'), 'base64' or 'escape'; the UTF8 encoding" },
57
- "row-value": { kind: "refused", what: "a row as a value outside the SELECT list, a cast to text and the json functions (compared, tested with IS NULL, grouped, sorted); a row of a row; a row of a table Mason copied in part", postgres: "compares, tests and groups a row by its columns", mason: "refused", wayOut: "compare, test or sort its columns" },
59
+ "row-value": { kind: "refused", what: "a row as a value outside the SELECT list, a cast to text and the json functions (compared, tested with IS NULL, grouped, sorted); a row of a row; a row of a table Mason synced in part", postgres: "compares, tests and groups a row by its columns", mason: "refused", wayOut: "compare, test or sort its columns" },
58
60
  "zone-abbreviation": { kind: "refused", what: "a time zone written as an abbreviation or a POSIX zone (`CST`, `IST`, `UTC+3`, `+05`) in AT TIME ZONE or date_trunc", postgres: "a fixed offset (`CST` is -06 all year; `UTC+3` is three hours WEST)", mason: "refused, written or held by a row (DuckDB reads several as another zone: `CST` as Chicago's, `IST` as India's, `UTC+3` as east)", wayOut: "an IANA zone name (America/Chicago, Asia/Kolkata), in any case; UT, Z and UTC are UTC", heldBy: "tests/duck-wire.test.ts" },
59
61
  "volatile-window": { kind: "refused", what: "a window ordered or partitioned by random() or another volatile function (a random row per group)", postgres: "a new value per row", mason: "refused (DuckDB refuses a volatile expression there, and computes a deterministic one once for every row)", wayOut: "SELECT …, row_number() OVER (ORDER BY r) FROM (SELECT *, random() AS r FROM t) AS s" },
60
62
  "numeric-special": { kind: "refused", what: "arithmetic on a numeric NaN or Infinity read from a text", postgres: "NaN and ±Infinity by its own rules (NaN + 1 is NaN)", mason: "refused when a row holds one", wayOut: "filter the special values out first (x <> 'NaN')" },
@@ -65,21 +67,26 @@ export const GAPS = {
65
67
  "infinite-interval": { kind: "refused", what: "an infinite interval read from a text (Postgres 17+)", postgres: "infinity, -infinity", mason: "refused when a row holds one (DuckDB's interval has no infinity)", wayOut: "filter it out first, or read it as text" },
66
68
  "relative-time-literal": { kind: "refused", what: "a relative time combined with anything else ('today 10:00', 'now'::date + … written as one literal), or read from a row as a date or a time", postgres: "the transaction's start (a row's value: when the row is read)", mason: "refused (a bare 'now', 'today', 'tomorrow', 'yesterday' literal is answered: the transaction's start at the Parse)", wayOut: "current_date + time '10:00', now(), current_date + 1" },
67
69
  // ---------------------------------------------------------------------------------------------- differs
68
- "limit-errors": { kind: "differs", what: "a select-list value that raises (`'x'::int`, a bad JSON number) in a row a LIMIT leaves out, where no index gives the ORDER BY", postgres: "raises: its plan sorts every row, its select list computed for each (through an index it stops at the limit and answers)", mason: "answers: a sorted, limited select list is computed for the rows the limit keeps (src/duck/late.ts), as Postgres's index scan does", wayOut: "none needed for an answer; filter the bad rows out to have neither raise" },
70
+ "limit-errors": { kind: "differs", what: "a value that raises (`'x'::int`, a bad JSON number) in a row a LIMIT leaves out: in the select list, where no index gives the ORDER BY; or in the argument of a LEFT JOIN of a list's rows ON TRUE (unnest(ARRAY[…, 'x'::int]), jsonb_each of a non-object), under a LIMIT with no ORDER BY", postgres: "the select list's raises: its plan sorts every row, its select list computed for each (through an index it stops at the limit and answers); the lateral's answers: it stops at the limit", mason: "the select list's answers: a sorted, limited select list is computed for the rows the limit keeps (src/duck/late.ts), as Postgres's index scan does; the lateral's raises: DuckDB computes its argument for a whole vector of rows (2,048) before the limit stops it", wayOut: "none needed for the select list's answer; filter the bad rows out to have neither raise" },
71
+ "limit-lateral-filter": { kind: "differs", what: "a LEFT JOIN of a json document's rows ON TRUE (json_each, jsonb_array_elements, …) under a LIMIT, or a row count an Execute sets on the wire, and no ORDER BY, with a filter above the join that is not pushed down to the base: a WHERE on the lateral's own column (d.key <> 'n'), one holding a bound parameter ($1) in a SELECT around the join, or one in a SELECT that reads the join through a CTE", postgres: "the first rows of the base, in its order, each row's members in theirs", mason: "the engine plans the lateral under such a filter as a delimiter join and hands the rows back in an order of its own, so the LIMIT keeps other rows (a filter on the base in the join's own SELECT, literal or parameter, or a literal one around it through a derived table, keeps Postgres's)", wayOut: "ORDER BY the base's key before the LIMIT, or filter the base in the SELECT that holds the join", heldBy: "tests/duck-wire.test.ts" },
72
+ "limit-lateral-argument": { kind: "differs", what: "a LEFT JOIN of a list's rows ON TRUE (unnest, json_each, jsonb_array_elements, …) under a LIMIT, or a row count an Execute sets on the wire, and no ORDER BY, whose argument holds a subquery (unnest(ARRAY(SELECT …))), or reads two or more columns of the row and no array (CASE WHEN t.kind = 'doc' THEN t.body END) in a SELECT whose list is a bare * or inside a join with a name of its own ((a LEFT JOIN … ON TRUE) AS j)", postgres: "the first rows of the base, in its order, each row's members in theirs", mason: "DuckDB plans such a lateral as a delimiter join and hands the rows back in an order of its own, so the LIMIT keeps other rows (elsewhere an argument of two columns is computed in a step of its own before the lateral, which a * would read as a column; a subquery is a delimiter join in every form)", wayOut: "name the columns instead of *, or ORDER BY the base's key before the LIMIT" },
73
+ "window-partition-order": { kind: "differs", what: "a window function that reads its frame in order (row_number, lag, lead, first_value, last_value, nth_value, ntile) over a window with PARTITION BY and no ORDER BY", postgres: "a partition's rows in the order its sort hands them back: in practice the order they came in", mason: "a partition's rows in the order DuckDB's partitioning hands them back, another one", wayOut: "ORDER BY in the window (OVER (PARTITION BY a ORDER BY b))" },
69
74
  "mixed-scale-numeric": { kind: "differs", what: "a numeric column whose values have several scales: an integer UNION ALL a numeric, lead(x, 1, 0) beside numeric(2,1) values, an unconstrained column holding 1 and 1.50", postgres: "each value its own scale: 1, 1.5", mason: "one scale per column: 1.0, 1.5", wayOut: "cast to numeric(p, s), or round(x, s)" },
70
75
  "unicode-16-case": { kind: "differs", what: "upper / lower of letters added in Unicode 16", postgres: "maps them (its own Unicode 16 tables)", mason: "leaves them as they are (DuckDB's ICU predates Unicode 16)", wayOut: "none needed for letters before Unicode 16" },
71
- "float-last-digit": { kind: "differs", what: "stddev / variance of a float, and ln, log, exp, power of a double: the last digit", postgres: "its algorithm and libm's", mason: "DuckDB's: equal to about 1e-15", wayOut: "round(x::numeric, 12)" },
76
+ "float-last-digit": { kind: "differs", what: "stddev / variance of a float, and ln, log, exp, power of a double: the last digit may differ", postgres: "its algorithm and its machine's libm", mason: "DuckDB's and its machine's: alike on some pairs of machines, apart in the 16th–17th digit on others", wayOut: "round(x::numeric, 12), or compare within a tolerance (abs(a - b) <= 1e-12 * abs(b))" },
72
77
  "negative-zero": { kind: "differs", what: "-0 beside 0 in greatest / least / percentile_disc", postgres: "keeps the one it met first", mason: "may answer the other", wayOut: "x + 0 makes -0 into 0" },
73
78
  "float-overflow": { kind: "differs", what: "exp / power of a double past its range, or so small it underflows", postgres: "an error (22003)", mason: "Infinity, or 0", wayOut: "bound the argument" },
74
79
  "min-max-scale-tie": { kind: "differs", what: "min / max of a numeric expression (a quotient) where equal values have different scales (0 / 7 and 0 / 1000000)", postgres: "the last of them it reads, so the scan order decides the trailing zeros", mason: "the one whose operands are least (min) or greatest (max)", wayOut: "round(min(a / b), n)" },
75
80
  "numeric-stddev": { kind: "differs", what: "stddev / variance of DISTINCT integers or numerics, or of a numeric declared without a precision or with more than 18 digits: the last digits", postgres: "exact numeric arithmetic, to its own scale", mason: "a double's 15 digits", wayOut: "round(stddev(x), 10)" },
81
+ "jsonb-equal-text": { kind: "differs", what: "GROUP BY or DISTINCT of jsonb values that are equal but written apart ('1.0' and '1.00', '{\"a\": 1}' and '{\"a\": 1.0}')", postgres: "one group, written as the value its plan reads first", mason: "one group, written as the value DuckDB reads first: either may be any of them", wayOut: "GROUP BY j::text where the text must decide", heldBy: "tests/duck-jsonb-order.test.ts" },
76
82
  "char-n-minmax": { kind: "differs", what: "greatest / least of a char(n) beside a text", postgres: "compares without the padding, answers the first of equal values", mason: "compares the padded text", wayOut: "cast the char(n) to text first" },
77
83
  "nested-exact-numeric": { kind: "differs", what: "an expression over a numeric a subquery computed exactly (a division, an avg, a round), outside that subquery: SELECT x * 2 FROM (SELECT a / 3 AS x …)", postgres: "exact digits at every step", mason: "the subquery's value as DuckDB computed it (a double's 15 digits) where the outer query computes with it; passed through as it is (SELECT *, s.x), it is exact", wayOut: "compute it in the outermost query, or round(…, n) inside" },
84
+ "show-all": { kind: "differs", what: "SHOW ALL", postgres: "every server setting with the source database's values", mason: "every setting the listener's catalog Postgres has, with this session's values laid over (search_path, TimeZone, ssl, …): Mason's settings, never your source database's", wayOut: "ask the source database for its settings (SHOW ALL or SHOW <name> there): Mason answers its own" },
78
85
  // ---------------------------------------------------------------------------------------------- limits
79
- "one-token": { kind: "limit", what: "access", postgres: "roles, grants, row-level security", mason: "every user (mason user) and token reads every copied table: no per-user or per-tenant scope", wayOut: "copy only the tables every user may read" },
80
- "poll-full-scan": { kind: "limit", what: "keeping current by the poll (mason start without --replication)", postgres: "-", mason: "the xmin poll scans every added table whole, every poll (no index covers xmin): ~13–17 s for 13 GiB", wayOut: "logical replication: mason start --replication" },
81
- "writes": { kind: "limit", what: "INSERT, UPDATE, DELETE, DDL, a read-write transaction", postgres: "answers", mason: "refused by name: the copy is read-only (a read-write transaction: 25006)", wayOut: "write to the source database; the copy follows" },
82
- "numeric-over-38-digits": { kind: "limit", what: "a numeric of more than 38 digits, or holding NaN or Infinity: a column, or a constant ('1e400'::numeric) inside DuckDB's arithmetic or a WHERE", postgres: "exact", mason: "a double (15 digits), said in the log for a column; a constant the outermost SELECT answers is exact", wayOut: "numeric(p, s) with p ≤ 38" },
86
+ roles: { kind: "limit", what: "access", postgres: "roles, grants on tables and columns, row-level security", mason: "a role (mason/roles.yml) grants a user tables and columns, held on the analysed tree and refused as Postgres refuses them (42501 permission denied for table …); information_schema and mason mcp list a role what it may read, pg_catalog still lists every synced column; a role's where: or through: filters a table's rows by the user's attributes (mason user add --set), every read of it filtered on the analysed tree and held by Postgres row security in the catalog behind a security barrier as a Postgres policy is (a statement's own expressions run only on the rows the filter keeps; timing is not guaranteed), a whole row or TABLESAMPLE of a filtered table refused, so is a grouping set relying on its key for a column it does not group, EXPLAIN of a statement reading one said in Mason's lines alone with no engine plan and EXPLAIN ANALYZE of it refused (either would say what the engine knows of every row), a user missing a value refused (42501) at every statement; a user with no role reads as mason/roles.yml's default: role, nothing when the file names none, every synced table when there is no file; a token reads every synced table", wayOut: "mason user add <name> --role <role> --set <attribute>=<value>; name the filtered table's columns in place of its whole row; read the full plan as a user with no row filter", heldBy: "tests/duck-roles-wire.test.ts" },
87
+ "poll-full-scan": { kind: "limit", what: "keeping current by the poll (a table's sync: poll <interval>)", postgres: "-", mason: "the xmin poll scans its table whole, every poll (no index covers xmin): ~13–17 s for 13 GiB", wayOut: "logical replication for that table: sync: replication on its entry (mason add <table> --sync replication)" },
88
+ "writes": { kind: "limit", what: "INSERT, UPDATE, DELETE, DDL, a read-write transaction", postgres: "answers", mason: "refused by name: the synced data is read-only (a read-write transaction: 25006)", wayOut: "write to the source database; the synced data follows" },
89
+ "numeric-over-38-digits": { kind: "limit", what: "a numeric of more than 38 digits, or holding NaN or Infinity: a column, or a constant ('1e400'::numeric) inside Mason's arithmetic or a WHERE", postgres: "exact", mason: "a double (15 digits), said in the log for a column; a constant the outermost SELECT answers is exact", wayOut: "numeric(p, s) with p ≤ 38" },
83
90
  "numeric-array": { kind: "limit", what: "an unconstrained numeric[] column", postgres: "exact elements", mason: "double precision elements (15 digits), said in the log", wayOut: "numeric(p, s)[]" },
84
91
  "numeric-parameter": { kind: "limit", what: "a numeric parameter ($1) compared in a WHERE", postgres: "exact", mason: "a double (exact to 15 digits)", wayOut: "a literal" },
85
92
  "log-literals": { kind: "limit", what: ".mason/mason.log's statement lines", postgres: "-", mason: "each statement DuckDB ran, whole, literals included", wayOut: "keep the log private" },
@@ -87,3 +94,10 @@ export const GAPS = {
87
94
  /** the words a runtime refusal carries for the oracle and a reader: `Mason gap <id>` */
88
95
  export const gapMark = (id) => `Mason gap ${id}`;
89
96
  export const GAP_MARK = /\bMason gap ([a-z0-9-]+)/;
97
+ /** The entry a runtime refusal's TEXT names (`gapMark`), or null: only for a refusal raised inside DuckDB (a Mason
98
+ * function's error, src/duck/functions.ts), which crosses DuckDB as its message alone (src/duck/store.ts pgErrorOf);
99
+ * a refusal built outside it carries its id on the error (src/api/postgres/server.ts gapRefusal). */
100
+ export function gapIn(text) {
101
+ const id = GAP_MARK.exec(text)?.[1];
102
+ return id !== undefined && Object.hasOwn(GAPS, id) ? id : null;
103
+ }