@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.
- package/README.md +147 -69
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog.js +258 -14
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/server.js +1290 -223
- package/dist/src/api/postgres/translate.js +81 -9
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/pg-wire.js +144 -38
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +55 -8
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +5 -3
- package/dist/src/cli/commands.js +843 -272
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/demo.js +158 -0
- package/dist/src/cli/dev.js +458 -125
- package/dist/src/cli/doctor.js +25 -12
- package/dist/src/cli/guides.js +103 -31
- package/dist/src/cli/init.js +159 -58
- package/dist/src/cli/machine.js +18 -12
- package/dist/src/cli/main.js +5 -5
- package/dist/src/cli/mcp.js +735 -0
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/pg-tls.js +13 -3
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +13 -12
- package/dist/src/cli/preflight.js +24 -14
- package/dist/src/cli/project.js +48 -7
- package/dist/src/cli/readiness.js +38 -4
- package/dist/src/cli/registry.js +57 -35
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +584 -0
- package/dist/src/cli/run-server.js +498 -133
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +10 -8
- package/dist/src/cli/serve.js +147 -40
- package/dist/src/cli/share-run.js +286 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +168 -0
- package/dist/src/cli/sources.js +290 -39
- package/dist/src/cli/supervise.js +12 -1
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui.js +9 -6
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user.js +496 -40
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +240 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/events.js +134 -0
- package/dist/src/duck/executor-worker.js +25 -11
- package/dist/src/duck/executor.js +107 -53
- package/dist/src/duck/explain.js +158 -29
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +73 -19
- package/dist/src/duck/gaps.js +25 -11
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/history.js +123 -0
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +10 -8
- package/dist/src/duck/kept-route.js +82 -2
- package/dist/src/duck/kept.js +211 -44
- package/dist/src/duck/pgtree.js +40 -0
- package/dist/src/duck/plan.js +120 -30
- package/dist/src/duck/publication.js +6 -6
- package/dist/src/duck/requests.js +265 -93
- package/dist/src/duck/rewrite.js +1193 -73
- package/dist/src/duck/row-filter.js +649 -0
- package/dist/src/duck/store.js +271 -159
- package/dist/src/duck/stream.js +10 -5
- package/dist/src/duck/sync.js +907 -275
- package/dist/src/duck/types.js +6 -5
- package/dist/src/duck/usage.js +324 -0
- package/dist/src/duck/users.js +64 -15
- package/dist/src/duck/verified.js +9 -0
- package/dist/src/duck/views.js +2 -3
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/log-buffer.js +68 -0
- package/dist/src/lib/pg-numeric.js +5 -2
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/table-sync.js +267 -0
- package/dist/src/lib/timezone.js +3 -2
- package/dist/src/web/app.css +706 -55
- package/dist/src/web/app.js +330 -258
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +84 -15
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +446 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +434 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +218 -0
- package/dist/src/web/shared.js +568 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +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 {
|
|
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
|
|
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.
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
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
|
-
/**
|
|
626
|
-
*
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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)));
|
package/dist/src/duck/gaps.js
CHANGED
|
@@ -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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
80
|
-
"poll-full-scan": { kind: "limit", what: "keeping current by the poll (
|
|
81
|
-
"writes": { kind: "limit", what: "INSERT, UPDATE, DELETE, DDL, a read-write transaction", postgres: "answers", mason: "refused by name: the
|
|
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
|
|
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
|
+
}
|