@mason-data/cli 0.6.2 → 0.7.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 +71 -12
- package/dist/src/api/postgres/catalog.js +36 -1
- package/dist/src/api/postgres/server.js +133 -32
- package/dist/src/cdc/pg-wire.js +144 -38
- package/dist/src/cli/background.js +29 -5
- package/dist/src/cli/command.js +4 -3
- package/dist/src/cli/commands.js +259 -127
- package/dist/src/cli/demo.js +149 -0
- package/dist/src/cli/dev.js +113 -20
- package/dist/src/cli/doctor.js +6 -3
- package/dist/src/cli/guides.js +32 -8
- package/dist/src/cli/init.js +58 -18
- package/dist/src/cli/machine.js +8 -3
- package/dist/src/cli/main.js +2 -2
- package/dist/src/cli/mcp.js +726 -0
- package/dist/src/cli/preflight.js +16 -7
- package/dist/src/cli/project.js +5 -3
- package/dist/src/cli/registry.js +8 -5
- package/dist/src/cli/run-server.js +29 -106
- package/dist/src/cli/serve.js +6 -1
- package/dist/src/cli/source-edit.js +116 -0
- package/dist/src/cli/sources.js +6 -4
- package/dist/src/demo/seed.js +237 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/executor-worker.js +7 -2
- package/dist/src/duck/executor.js +67 -26
- package/dist/src/duck/explain.js +128 -9
- package/dist/src/duck/functions.js +60 -5
- package/dist/src/duck/gaps.js +9 -4
- 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 +206 -39
- package/dist/src/duck/pgtree.js +40 -0
- package/dist/src/duck/plan.js +21 -8
- package/dist/src/duck/rewrite.js +1168 -68
- package/dist/src/duck/store.js +132 -56
- package/dist/src/duck/sync.js +90 -25
- package/dist/src/duck/verified.js +6 -0
- package/dist/src/lib/timezone.js +3 -2
- package/dist/src/web/app.css +11 -0
- package/dist/src/web/app.js +167 -12
- package/dist/src/web/index.html +6 -1
- package/package.json +1 -1
|
@@ -10,13 +10,15 @@
|
|
|
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";
|
|
@@ -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,6 +489,7 @@ 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;
|
|
@@ -598,6 +606,33 @@ const steps = (json, either) => JSON.parse(json)
|
|
|
598
606
|
export function registerLibcOrder(conn, order) {
|
|
599
607
|
scalar(conn, "__mason_libc_kept", [VARCHAR], VARCHAR, ([s]) => libcKept(order, String(s)));
|
|
600
608
|
scalar(conn, "__mason_libc_key", [VARCHAR, VARCHAR], VARCHAR, ([s, icu]) => libcKey(order, String(s), String(icu)));
|
|
609
|
+
// jsonb's order under it (src/duck/jsonb-order.ts): the characters of each string ICU's levels read, then the key
|
|
610
|
+
// from ICU's keys of them (src/duck/rewrite.ts jsonbKey hands them in)
|
|
611
|
+
keyScalar(conn, JSONB_KEY_FUNCTIONS.libc.strings, [VARCHAR], LIST(VARCHAR), ([j]) => listValue(jsonbLibcKept(order, String(j))));
|
|
612
|
+
keyScalar(conn, JSONB_KEY_FUNCTIONS.libc.key, [VARCHAR, LIST(VARCHAR)], BLOB, ([j, keys]) => new DuckDBBlobValue(jsonbKey(String(j), { kind: "libc", order, icuKeys: icuKeysOf(keys) })));
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* A FUNCTION OF A JSONB'S KEY (src/duck/jsonb-order.ts `JSONB_KEY_FUNCTIONS`), and its stored twin (`STORED_SUFFIX`),
|
|
616
|
+
* which a write of the copy computes a stored key with (src/duck/kept.ts `orderKeySql`): the same body, answering NULL
|
|
617
|
+
* where it raises — a write of the copy (the stream's, the poll's) must not fail over one row's document. The stored
|
|
618
|
+
* 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
|
|
619
|
+
* (a libc string the order does not cover: `libc-uncovered`), as the statement would without the stored key.
|
|
620
|
+
*/
|
|
621
|
+
function keyScalar(conn, name, params, returns, body) {
|
|
622
|
+
scalar(conn, name, params, returns, body);
|
|
623
|
+
scalar(conn, `${name}${STORED_SUFFIX}`, params, returns, (args) => {
|
|
624
|
+
try {
|
|
625
|
+
return body(args);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
return null;
|
|
629
|
+
} // the row's mark: its read computes it, and raises (above)
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
/** the ICU keys DuckDB made of a jsonb's strings (a NULL among them is a Mason bug: every string has one) */
|
|
633
|
+
function icuKeysOf(v) {
|
|
634
|
+
return itemsOf(v).map((k) => { if (k === null)
|
|
635
|
+
throw new Error("an ICU key of a jsonb's string is NULL: a Mason bug"); return k; });
|
|
601
636
|
}
|
|
602
637
|
/**
|
|
603
638
|
* EVERY CALL CARRIES ITS SESSION, FIRST (the test server: a client leaving mid-statement hung the process
|
|
@@ -618,6 +653,9 @@ export const CANCELLED = "mason[57014]: canceling statement due to user request"
|
|
|
618
653
|
/** how many vectors each function was called with (a call is a hop to the main thread): what a test or a measurement
|
|
619
654
|
* reads to know whether a statement reached a function at all (the rewrite answers a `->>` natively where it can) */
|
|
620
655
|
export const FUNCTION_CALLS = new Map();
|
|
656
|
+
/** how many rows each function was handed, across its calls: what a plan's cost in function calls is measured by,
|
|
657
|
+
* whatever the chunking (tests/duck-plans.test.ts) */
|
|
658
|
+
export const FUNCTION_ROWS = new Map();
|
|
621
659
|
/** the cancel table the calls read: the executor's, shared with the listener (`useCancelTable`), else one of this thread's own */
|
|
622
660
|
let cancels = cancelTable();
|
|
623
661
|
export function useCancelTable(t) { cancels = t; }
|
|
@@ -647,10 +685,27 @@ export function registerFunctions(conn) {
|
|
|
647
685
|
scalar(conn, "__mason_json_get", [VARCHAR, VARCHAR, BOOLEAN, BOOLEAN, BOOLEAN], VARCHAR, ([j, s, either, canonical, asText]) => jsonGet(String(j), steps(String(s), either === true), canonical === true, asText === true));
|
|
648
686
|
// the same, as a stored JSON path keeps it (keptJsonValue, src/duck/kept.ts): what the copy's writes compute
|
|
649
687
|
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
|
-
|
|
688
|
+
// a document's rows as a list each (elements, keys, values); `orNull` (the last argument): never empty, a NULL or
|
|
689
|
+
// empty document's list one NULL — a LEFT JOIN LATERAL … ON TRUE's one row of NULLs, as an INNER lateral DuckDB runs
|
|
690
|
+
// in its rows' order and stops at a LIMIT (src/duck/rewrite.ts `leftLateral`)
|
|
691
|
+
const rows = (j, orNull, list) => {
|
|
692
|
+
const items = j === null || j === undefined ? null : list(String(j));
|
|
693
|
+
return orNull === true && (items === null || items.length === 0) ? listValue([null]) : items === null ? null : listValue(items);
|
|
694
|
+
};
|
|
695
|
+
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" });
|
|
696
|
+
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" });
|
|
697
|
+
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
698
|
scalar(conn, "__mason_jsonb_text", [VARCHAR], VARCHAR, ([j]) => jsonbText(String(j)));
|
|
699
|
+
// jsonb's order and equality as bytes (src/duck/jsonb-order.ts): under C, and under ICU from ICU's keys of the
|
|
700
|
+
// document's strings, which the rewrite has DuckDB make of `__mason_jsonb_strings` (src/duck/rewrite.ts jsonbKey)
|
|
701
|
+
keyScalar(conn, JSONB_KEY_FUNCTIONS.bytes.key, [VARCHAR], BLOB, ([j]) => new DuckDBBlobValue(jsonbKey(String(j), { kind: "bytes" })));
|
|
702
|
+
// a stored column's marked row read exact raises its error (src/duck/kept-route.ts markedError): never DuckDB's
|
|
703
|
+
// volatile error(), which a window's ORDER BY refuses
|
|
704
|
+
const marked = ([, code, message]) => { throw new PgEvalError(String(code), String(message)); };
|
|
705
|
+
scalar(conn, MARKED_FUNCTIONS.key, [VARCHAR, VARCHAR, VARCHAR], BLOB, marked);
|
|
706
|
+
scalar(conn, MARKED_FUNCTIONS.value, [VARCHAR, VARCHAR, VARCHAR], VARCHAR, marked);
|
|
707
|
+
keyScalar(conn, JSONB_KEY_FUNCTIONS.icu.strings, [VARCHAR], LIST(VARCHAR), ([j]) => listValue(jsonbStrings(String(j))));
|
|
708
|
+
keyScalar(conn, JSONB_KEY_FUNCTIONS.icu.key, [VARCHAR, LIST(VARCHAR)], BLOB, ([j, keys]) => new DuckDBBlobValue(jsonbKey(String(j), { kind: "icu", icuKeys: icuKeysOf(keys) })));
|
|
654
709
|
// jsonb's @>, ?, ?| and ?& (DuckDB's json_contains and json_exists answer otherwise on arrays), json(b)_array_length
|
|
655
710
|
scalar(conn, "__mason_jsonb_contains", [VARCHAR, VARCHAR], BOOLEAN, ([a, b]) => jsonbContains(String(a), String(b)));
|
|
656
711
|
scalar(conn, "__mason_jsonb_exists", [VARCHAR, VARCHAR], BOOLEAN, ([j, k]) => jsonbExists(String(j), String(k)));
|
package/dist/src/duck/gaps.js
CHANGED
|
@@ -33,11 +33,12 @@ export const GAPS = {
|
|
|
33
33
|
"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
34
|
"extract-julian": { kind: "refused", what: "extract(julian FROM …)", postgres: "answers", mason: "refused", wayOut: "extract(epoch FROM …) / 86400 + 2440587.5" },
|
|
35
35
|
"with-ordinality": { kind: "refused", what: "a function in FROM … WITH ORDINALITY", postgres: "numbers the rows", mason: "refused", wayOut: "row_number() OVER () instead" },
|
|
36
|
+
"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
37
|
"multidim-array": { kind: "refused", what: "a multi-dimensional array literal", postgres: "answers", mason: "refused", wayOut: "a one-dimensional array" },
|
|
37
38
|
"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
39
|
"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
40
|
"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" },
|
|
41
|
+
"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
42
|
"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
43
|
"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
44
|
"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 +46,7 @@ export const GAPS = {
|
|
|
45
46
|
"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
47
|
"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
48
|
"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: "
|
|
49
|
+
"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
50
|
"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
51
|
"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
52
|
"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" },
|
|
@@ -65,14 +66,18 @@ export const GAPS = {
|
|
|
65
66
|
"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
67
|
"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
68
|
// ---------------------------------------------------------------------------------------------- differs
|
|
68
|
-
"limit-errors": { kind: "differs", what: "a
|
|
69
|
+
"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" },
|
|
70
|
+
"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 DuckDB does not push 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: "DuckDB 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" },
|
|
71
|
+
"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" },
|
|
72
|
+
"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
73
|
"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
74
|
"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
|
|
75
|
+
"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
76
|
"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
77
|
"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
78
|
"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
79
|
"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)" },
|
|
80
|
+
"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
81
|
"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
82
|
"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" },
|
|
78
83
|
// ---------------------------------------------------------------------------------------------- limits
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONB'S ORDER AND EQUALITY AS BYTES (a BI tool sorts any column a user clicks: `ORDER BY "e"."tags" DESC` over a jsonb
|
|
3
|
+
* column was refused until this). Postgres orders and equates jsonb by `compareJsonbContainers` (jsonb_util.c), which
|
|
4
|
+
* walks both values token by token and stops at the first difference:
|
|
5
|
+
* - by TYPE first, at any depth: null < string < number < boolean < array < object (jbvType's order);
|
|
6
|
+
* - an array with more elements sorts higher, an object with more pairs higher; of equal counts, element by element,
|
|
7
|
+
* pair by pair in STORAGE order (a key shorter in bytes first, then by its bytes; a key held twice keeps its LAST
|
|
8
|
+
* value), each pair's key then its value;
|
|
9
|
+
* - a number as a numeric (`10 > 9.5`, `1.0 = 1.00`, `1e2 = 100`, `-0 = 0`), a boolean false < true, a string — a
|
|
10
|
+
* key's too — under the DATABASE's default collation (`varstr_cmp(..., DEFAULT_COLLATION_OID)`), whatever a
|
|
11
|
+
* column's own collation, ties broken by the bytes;
|
|
12
|
+
* - THE RAW SCALAR: a scalar at the top is stored as a one-element array marked `rawScalar`, and the two are compared
|
|
13
|
+
* as arrays — the marks first (a raw scalar below an array), then, overriding that, the counts — so `[] < 1 < [1]`
|
|
14
|
+
* (the source's comment: "a mild anomaly that an empty top level array sorts less than null"), and every top-level
|
|
15
|
+
* scalar sorts below every object.
|
|
16
|
+
* Equality is that comparison's 0 (`jsonb_eq`), so it is the same key, never the canonical text: `1.0` and `1.00` are
|
|
17
|
+
* one jsonb written two ways.
|
|
18
|
+
*
|
|
19
|
+
* The key is one byte string whose plain byte order (DuckDB's BLOB comparison: bytes, then the shorter first) is that
|
|
20
|
+
* order, and whose equality is that equality. Every value is prefix-free (it ends where its own encoding says), so a
|
|
21
|
+
* container is its tag, its count and its members one after another, and the first differing byte is the first token
|
|
22
|
+
* Postgres finds different:
|
|
23
|
+
* - a tag per type, in jbvType's order: null 0x10, string 0x20, number 0x30, boolean 0x40, array 0x50, object 0x60;
|
|
24
|
+
* - an array: its tag, its count (4 bytes, big-endian), then 0x00 for a raw scalar and 0x01 for an array (the count
|
|
25
|
+
* first: it decides before the mark), then each element; an object: its tag, its count, each pair's key and value;
|
|
26
|
+
* - a number: 0x80 for zero; else a sign byte (0x81 positive, 0x7f negative), the decimal exponent E of 0.d₁d₂… × 10^E
|
|
27
|
+
* (4 bytes, biased), the digits without leading or trailing zeros, a 0x00 — every byte after the sign inverted for a
|
|
28
|
+
* negative number, so a larger magnitude sorts lower;
|
|
29
|
+
* - a string: the collation's key of it, then a 0x00 (a jsonb string holds no NUL): its UTF-8 bytes under C (`bytes`);
|
|
30
|
+
* under ICU, ICU's sort key (DuckDB's `icu_sort_key`, which the rewrite hands in) and the bytes' hex; under a libc
|
|
31
|
+
* locale the measured key (src/duck/libc-order.ts `libcKey`) and the bytes' hex, as the rewrite sorts text
|
|
32
|
+
* (src/duck/rewrite.ts `libcKey`).
|
|
33
|
+
* A string's collation key is computed by DuckDB's ICU for the ICU and libc kinds, which a JavaScript function cannot
|
|
34
|
+
* call: the rewrite passes the document's strings' ICU keys as a list (`jsonbStrings` lists the strings, in the order
|
|
35
|
+
* this walk meets them; `icuKeys` the list DuckDB made of them). Held to Postgres by tests/duck-jsonb-order.test.ts
|
|
36
|
+
* (PGlite's order) and the oracle (tests/integration/duck-oracle.itest.ts, every collation it is run under).
|
|
37
|
+
*/
|
|
38
|
+
import { Buffer } from "node:buffer";
|
|
39
|
+
import { parseJson, storedPairs } from "./jsonb.js";
|
|
40
|
+
import { libcKept, libcKey } from "./libc-order.js";
|
|
41
|
+
const TAG = { null: 0x10, str: 0x20, num: 0x30, bool: 0x40, arr: 0x50, obj: 0x60 };
|
|
42
|
+
const BIAS = 0x80000000;
|
|
43
|
+
/** every string of the value — each object's keys and every string value — in the order the key's walk meets them */
|
|
44
|
+
function eachString(v, out) {
|
|
45
|
+
switch (v.t) {
|
|
46
|
+
case "str":
|
|
47
|
+
out.push(v.v);
|
|
48
|
+
return;
|
|
49
|
+
case "arr":
|
|
50
|
+
for (const x of v.v)
|
|
51
|
+
eachString(x, out);
|
|
52
|
+
return;
|
|
53
|
+
case "obj":
|
|
54
|
+
for (const [k, x] of storedPairs(v.v)) {
|
|
55
|
+
out.push(k);
|
|
56
|
+
eachString(x, out);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
default: return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* THE FUNCTIONS A KEY IS MADE BY, per collation kind (src/duck/functions.ts registers them): the key's, and the one
|
|
64
|
+
* listing the strings DuckDB makes ICU keys of (none under C). A statement's key (src/duck/rewrite.ts `jsonbKey`) and a
|
|
65
|
+
* stored one (src/duck/kept.ts `orderKeySql`) both read this table, so the two are one computation by construction;
|
|
66
|
+
* a stored key calls each name with `STORED_SUFFIX`, the same function answering NULL where this one raises.
|
|
67
|
+
*/
|
|
68
|
+
export const JSONB_KEY_FUNCTIONS = {
|
|
69
|
+
bytes: { key: "__mason_jsonb_key", strings: null },
|
|
70
|
+
icu: { key: "__mason_jsonb_key_icu", strings: "__mason_jsonb_strings" },
|
|
71
|
+
libc: { key: "__mason_jsonb_key_libc", strings: "__mason_jsonb_libc_kept" },
|
|
72
|
+
};
|
|
73
|
+
export const STORED_SUFFIX = "_stored";
|
|
74
|
+
/**
|
|
75
|
+
* THE KEY'S VERSION: a stored key (src/duck/kept.ts `orderId`) is this encoding's, so a release that changes a byte of
|
|
76
|
+
* it — a tag, the number's form, a string's — bumps this, and a key stored by an earlier one is dropped at the start
|
|
77
|
+
* and stored again (tests/duck-jsonb-order.test.ts holds the encoding; a change it catches is a change of this).
|
|
78
|
+
*/
|
|
79
|
+
export const KEY_VERSION = 1;
|
|
80
|
+
/** The strings of a jsonb's text in the key's order (what DuckDB's ICU makes the keys of). */
|
|
81
|
+
export function jsonbStrings(text) {
|
|
82
|
+
const out = [];
|
|
83
|
+
eachString(parseJson(text), out);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/** whether a jsonb's strings hold a character a libc order does not cover: what its key refuses by name
|
|
87
|
+
* (`libc-uncovered`, src/duck/libc-order.ts `libcKey`), told from any other failure of the key (src/duck/kept.ts) */
|
|
88
|
+
export function libcUncovered(order, text) {
|
|
89
|
+
return jsonbStrings(text).some((s) => { for (const ch of s)
|
|
90
|
+
if (order.uncovered[ch.codePointAt(0)])
|
|
91
|
+
return true; return false; });
|
|
92
|
+
}
|
|
93
|
+
/** The strings ICU's accent and case levels read for a libc key of each (src/duck/libc-order.ts `libcKept`). */
|
|
94
|
+
export function jsonbLibcKept(order, text) {
|
|
95
|
+
return jsonbStrings(text).map((s) => libcKept(order, s));
|
|
96
|
+
}
|
|
97
|
+
const u32 = (n) => [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];
|
|
98
|
+
/**
|
|
99
|
+
* A JSON number's text as its key: zero, or its sign, exponent and digits (the value's, exactly: no rounding through a
|
|
100
|
+
* double, and the scale left out — `1.0` and `1.00` are one number). Read from the text itself, never through a
|
|
101
|
+
* BigInt of its digits (`1e100000` is one digit and an exponent).
|
|
102
|
+
*/
|
|
103
|
+
export function numberKey(text) {
|
|
104
|
+
const m = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(text);
|
|
105
|
+
if (m === null)
|
|
106
|
+
throw new Error(`not a JSON number: ${JSON.stringify(text)}`);
|
|
107
|
+
const fraction = m[3] ?? "";
|
|
108
|
+
// the value is all × 10^(exponent - fraction's length), and so 0.<digits> × 10^E with E = all's length less that
|
|
109
|
+
const all = (m[2] + fraction).replace(/^0+/, "");
|
|
110
|
+
const digits = all.replace(/0+$/, "");
|
|
111
|
+
if (digits === "")
|
|
112
|
+
return [0x80];
|
|
113
|
+
const e = all.length - fraction.length + Number(m[4] ?? 0);
|
|
114
|
+
const body = [...u32(e + BIAS), ...[...digits].map((d) => d.charCodeAt(0)), 0x00];
|
|
115
|
+
return m[1] === "-" ? [0x7f, ...body.map((b) => 0xff - b)] : [0x81, ...body];
|
|
116
|
+
}
|
|
117
|
+
const hex = (s) => Buffer.from(s, "utf8").toString("hex");
|
|
118
|
+
/** The key of a jsonb's text under the database's collation (the module's header says what it encodes). */
|
|
119
|
+
export function jsonbKey(text, c) {
|
|
120
|
+
const out = [];
|
|
121
|
+
let at = 0;
|
|
122
|
+
const icuKey = () => {
|
|
123
|
+
if (c.kind === "bytes")
|
|
124
|
+
throw new Error("a C collation's key needs no ICU key");
|
|
125
|
+
const k = c.icuKeys[at++];
|
|
126
|
+
if (k === undefined)
|
|
127
|
+
throw new Error(`the jsonb holds more strings than the ${c.icuKeys.length} ICU keys handed in: a Mason bug (the rewrite's list and this walk disagree)`);
|
|
128
|
+
return k;
|
|
129
|
+
};
|
|
130
|
+
const str = (s) => {
|
|
131
|
+
const key = c.kind === "bytes" ? s : c.kind === "icu" ? `${icuKey()} ${hex(s)}` : `${libcKey(c.order, s, icuKey())} ${hex(s)}`;
|
|
132
|
+
for (const b of Buffer.from(key, "utf8"))
|
|
133
|
+
out.push(b);
|
|
134
|
+
out.push(0x00);
|
|
135
|
+
};
|
|
136
|
+
const value = (v) => {
|
|
137
|
+
switch (v.t) {
|
|
138
|
+
case "null":
|
|
139
|
+
out.push(TAG.null);
|
|
140
|
+
return;
|
|
141
|
+
case "bool":
|
|
142
|
+
out.push(TAG.bool, v.v ? 1 : 0);
|
|
143
|
+
return;
|
|
144
|
+
case "num":
|
|
145
|
+
out.push(TAG.num, ...numberKey(v.text));
|
|
146
|
+
return;
|
|
147
|
+
case "str":
|
|
148
|
+
out.push(TAG.str);
|
|
149
|
+
str(v.v);
|
|
150
|
+
return;
|
|
151
|
+
case "arr":
|
|
152
|
+
out.push(TAG.arr, ...u32(v.v.length), 0x01);
|
|
153
|
+
for (const x of v.v)
|
|
154
|
+
value(x);
|
|
155
|
+
return;
|
|
156
|
+
case "obj": {
|
|
157
|
+
const pairs = storedPairs(v.v);
|
|
158
|
+
out.push(TAG.obj, ...u32(pairs.length));
|
|
159
|
+
for (const [k, x] of pairs) {
|
|
160
|
+
str(k);
|
|
161
|
+
value(x);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const root = parseJson(text);
|
|
168
|
+
// a scalar at the top is a raw scalar: a one-element array marked as one (0x00, below an array's 0x01)
|
|
169
|
+
if (root.t !== "arr" && root.t !== "obj")
|
|
170
|
+
out.push(TAG.arr, ...u32(1), 0x00);
|
|
171
|
+
value(root);
|
|
172
|
+
if (c.kind !== "bytes" && at !== c.icuKeys.length)
|
|
173
|
+
throw new Error(`the jsonb holds ${at} strings, ${c.icuKeys.length} ICU keys were handed in: a Mason bug (the rewrite's list and this walk disagree)`);
|
|
174
|
+
return Buffer.from(out);
|
|
175
|
+
}
|
package/dist/src/duck/jsonb.js
CHANGED
|
@@ -158,11 +158,19 @@ export function quoteJson(s) {
|
|
|
158
158
|
}
|
|
159
159
|
return `${out}"`;
|
|
160
160
|
}
|
|
161
|
-
/** jsonb's key order: shorter first, then by the UTF-8 bytes */
|
|
161
|
+
/** jsonb's key order: shorter first, then by the UTF-8 bytes (lengthCompareJsonbStringValue) */
|
|
162
162
|
function keyOrder(a, b) {
|
|
163
163
|
const x = Buffer.from(a, "utf8"), y = Buffer.from(b, "utf8");
|
|
164
164
|
return x.length - y.length || Buffer.compare(x, y);
|
|
165
165
|
}
|
|
166
|
+
/** an object's pairs as jsonb stores them, in its key order: a key held twice keeps its LAST value
|
|
167
|
+
* (uniqueifyJsonbObject). What jsonb writes and what its order compares (src/duck/jsonb-order.ts) both read it. */
|
|
168
|
+
export function storedPairs(members) {
|
|
169
|
+
const last = new Map();
|
|
170
|
+
for (const [k, x] of members)
|
|
171
|
+
last.set(k, x);
|
|
172
|
+
return [...last.keys()].sort(keyOrder).map((k) => [k, last.get(k)]);
|
|
173
|
+
}
|
|
166
174
|
/** a parsed value as jsonb writes it */
|
|
167
175
|
export function writeJson(v) {
|
|
168
176
|
switch (v.t) {
|
|
@@ -171,13 +179,7 @@ export function writeJson(v) {
|
|
|
171
179
|
case "num": return numericText(parseNumeric(v.text));
|
|
172
180
|
case "str": return quoteJson(v.v);
|
|
173
181
|
case "arr": return `[${v.v.map(writeJson).join(", ")}]`;
|
|
174
|
-
case "obj": {
|
|
175
|
-
// a duplicate key keeps its LAST value (jsonb's uniqueifyJsonbObject)
|
|
176
|
-
const last = new Map();
|
|
177
|
-
for (const [k, x] of v.v)
|
|
178
|
-
last.set(k, x);
|
|
179
|
-
return `{${[...last.keys()].sort(keyOrder).map((k) => `${quoteJson(k)}: ${writeJson(last.get(k))}`).join(", ")}}`;
|
|
180
|
-
}
|
|
182
|
+
case "obj": return `{${storedPairs(v.v).map(([k, x]) => `${quoteJson(k)}: ${writeJson(x)}`).join(", ")}}`;
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
/** A JSON text as Postgres's jsonb writes it; throws a json syntax error for a text that is no JSON. */
|
|
@@ -32,7 +32,7 @@ export function canonicalRead(r) {
|
|
|
32
32
|
}
|
|
33
33
|
/** a read as one string: the request log's key (its `->>` or `->` included: the policy counts both for one path) */
|
|
34
34
|
export const readKey = (r) => { const c = canonicalRead(r); return JSON.stringify([c.source, c.column, c.steps, c.either, c.canonical, c.asText]); };
|
|
35
|
-
/** a read's key, back (null for a string that is not one) */
|
|
35
|
+
/** a read's key, back (null for a string that is not one, and for a read of a kind this Mason does not know: `knownSteps`) */
|
|
36
36
|
export function readOfKey(key) {
|
|
37
37
|
let v;
|
|
38
38
|
try {
|
|
@@ -46,8 +46,27 @@ export function readOfKey(key) {
|
|
|
46
46
|
const [source, column, steps, either, canonical, asText] = v;
|
|
47
47
|
if (typeof source !== "string" || typeof column !== "string" || typeof steps !== "string" || typeof either !== "boolean" || typeof canonical !== "boolean" || typeof asText !== "boolean")
|
|
48
48
|
return null;
|
|
49
|
+
if (!knownSteps(steps))
|
|
50
|
+
return null;
|
|
49
51
|
return canonicalRead({ source, column, steps, either, canonical, asText });
|
|
50
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* WHETHER A READ'S STEPS ARE A KIND THIS MASON KNOWS: a path's (a JSON array of keys and indexes) or a jsonb's key
|
|
55
|
+
* (`isKeyRead`). A request log or a ledger written by another release may hold a kind this one does not know (an
|
|
56
|
+
* earlier release met `equal:…` and `order:…` and failed every write of the table, and its status, parsing them as a
|
|
57
|
+
* path): such a read is INERT, never fatal — not counted (`readOfKey`), its ledger row dropped at the start with its
|
|
58
|
+
* column (src/duck/kept.ts `open`), and said raw (`pathText`).
|
|
59
|
+
*/
|
|
60
|
+
export function knownSteps(steps) {
|
|
61
|
+
if (isKeyRead({ steps }))
|
|
62
|
+
return true;
|
|
63
|
+
try {
|
|
64
|
+
return Array.isArray(JSON.parse(steps));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
} // not JSON: not a path, so not a kind known here
|
|
69
|
+
}
|
|
51
70
|
/**
|
|
52
71
|
* THE STORED PATH A READ IS ANSWERED FROM: its canonical read (`canonicalRead`) without `->>`. One column serves a
|
|
53
72
|
* path's `->` and its `->>` (the test server: a popular key was stored twice, as `-> 'k'` and `->> 'k'`, two
|
|
@@ -72,9 +91,48 @@ export const hiddenName = (r) => `${HIDDEN_PREFIX}${createHash("sha256").update(
|
|
|
72
91
|
* table name, so `public.orders.id` and `orders.id` still bind.
|
|
73
92
|
*/
|
|
74
93
|
export const VISIBLE_CATALOG = "__mason_visible";
|
|
94
|
+
/**
|
|
95
|
+
* A JSONB'S ORDER, AS A READ (src/duck/jsonb-order.ts; docs/DESIGN.md "jsonb's order"): `ORDER BY j`, `GROUP BY j`,
|
|
96
|
+
* `j = k` compare a key of the whole document, computed per row by a function — 19 s for `ORDER BY j LIMIT 100` over
|
|
97
|
+
* 20 M rows under C, 83 s under a libc locale, where the same sort from a stored key takes 0.1 s. So the key is a read
|
|
98
|
+
* like a path's, recorded, weighed and stored by the same keeper: its `steps` is `ORDER_STEPS` (never a path's, which
|
|
99
|
+
* is a JSON array). The rewrite records it as `ORDER_STEPS`; the keeper stores it as `ORDER_STEPS:<the collation's
|
|
100
|
+
* id>` (src/duck/kept.ts `own`), because a string's key is its collation's, so a key stored under another collation is
|
|
101
|
+
* another column, never read.
|
|
102
|
+
*/
|
|
103
|
+
export const ORDER_STEPS = "order";
|
|
104
|
+
/** a stored key's marks (hex, DuckDB's `unhex`): a NULL document's, and a row whose key raised, computed when read. No
|
|
105
|
+
* key begins with a 0x00 (src/duck/jsonb-order.ts: every key starts with an array's or an object's tag) */
|
|
106
|
+
export const KEY_NULL_HEX = "00";
|
|
107
|
+
export const KEY_RECOMPUTE_HEX = "0021";
|
|
108
|
+
export const isOrderRead = (r) => r.steps === ORDER_STEPS || r.steps.startsWith(`${ORDER_STEPS}:`);
|
|
109
|
+
/**
|
|
110
|
+
* A JSONB'S EQUALITY, AS A READ, under a LIBC collation alone (src/duck/rewrite.ts `equalKey`): `GROUP BY j`, `DISTINCT
|
|
111
|
+
* j`, `j = k`, `j IN (…)`, a join on `=`, `count(DISTINCT j)` compare the C key — a libc order's key refuses a string
|
|
112
|
+
* its measured order does not cover, where equality never needed an order (docs/DESIGN.md "jsonb's order"), so the
|
|
113
|
+
* order's stored key cannot serve them, and over 20 M rows a GROUP BY computing the C key per row took 32–45 s of CPU
|
|
114
|
+
* (34–77 s wall; docs/DESIGN.md "Under libc, the C key stored for equality"), 0.15–0.23 s from it. So the C key
|
|
115
|
+
* is a read of its own, stored beside the order's as `EQUAL_STEPS:<id>` (src/duck/kept.ts `equalId`). Under C and ICU
|
|
116
|
+
* the order's key equates exactly and never refuses, so equality reads IT and this read is never recorded nor stored
|
|
117
|
+
* (src/duck/kept.ts `own` folds one into the order's).
|
|
118
|
+
*/
|
|
119
|
+
export const EQUAL_STEPS = "equal";
|
|
120
|
+
export const isEqualRead = (r) => r.steps === EQUAL_STEPS || r.steps.startsWith(`${EQUAL_STEPS}:`);
|
|
121
|
+
/** a jsonb's key, the order's or the equality's: a BLOB column of the key's bytes, its marks `KEY_NULL_HEX` and
|
|
122
|
+
* `KEY_RECOMPUTE_HEX`, stored under an id of what its bytes depend on */
|
|
123
|
+
export const isKeyRead = (r) => isOrderRead(r) || isEqualRead(r);
|
|
75
124
|
/** a read as a developer writes it: `payload ->> 'customer_id'`, `payload #> '{a,0}'`; a stored path (no `asText`) as
|
|
76
|
-
* its `->` read
|
|
125
|
+
* its `->` read; a jsonb's order and equality as what reads each (under libc the order's key sorts and the C key
|
|
126
|
+
* equates, two columns: `mason status` names each by its own) */
|
|
77
127
|
export function pathText(r) {
|
|
128
|
+
// a kind this Mason does not know (another release's): said as it is stored, never parsed (`knownSteps`)
|
|
129
|
+
if (!knownSteps(r.steps))
|
|
130
|
+
return `${r.column} (stored as ${r.steps}: a kind this Mason does not know)`;
|
|
131
|
+
if (isEqualRead(r))
|
|
132
|
+
return `GROUP BY ${r.column} (its jsonb equality: grouped, DISTINCT, = and IN)`;
|
|
133
|
+
if (isOrderRead(r))
|
|
134
|
+
return r.steps.startsWith(`${ORDER_STEPS}:libc:`) ? `ORDER BY ${r.column} (its jsonb order: sorted, and compared by < and >)`
|
|
135
|
+
: `ORDER BY ${r.column} (its jsonb order: sorted, grouped and compared)`;
|
|
78
136
|
const steps = JSON.parse(r.steps);
|
|
79
137
|
const op = (one) => one ? (r.asText ? "->>" : "->") : (r.asText ? "#>>" : "#>");
|
|
80
138
|
if (steps.length === 1 && !r.either) {
|
|
@@ -84,6 +142,28 @@ export function pathText(r) {
|
|
|
84
142
|
const element = (s) => typeof s === "number" || /^[A-Za-z0-9_]+$/.test(s) ? String(s) : `"${String(s).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
85
143
|
return `${r.column} ${op(false)} '{${steps.map(element).join(",").replaceAll("'", "''")}}'`;
|
|
86
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* WHAT AN EXACT READ RAISES AT A MARK (src/duck/rewrite.ts `keptRead`, `keptKey`): a row marked to be computed
|
|
147
|
+
* (`KEPT_RECOMPUTE`, `KEY_RECOMPUTE_HEX`) written AFTER the path was counted exact — by the stream, the poll or a copy's
|
|
148
|
+
* side table, before the detector (src/duck/kept.ts `check`) counts it and turns the reads to the CASE — is never read
|
|
149
|
+
* as a value: the exact read raises at it, by the column alone. A jsonb's key under a libc collation is marked where
|
|
150
|
+
* its strings hold a character the measured order does not cover, the gap the statement's own key refuses by name
|
|
151
|
+
* (`libc-uncovered`); any other mark is a Mason bug. Once counted (a minute at most), the CASE computes the row from its
|
|
152
|
+
* document, and raises the function's own error (the character named) or answers. Raised by `MARKED_FUNCTIONS`, never
|
|
153
|
+
* DuckDB's `error()`, which is volatile (a window's ORDER BY refuses it: the eager oracle's `rank() OVER (ORDER BY j)`).
|
|
154
|
+
*/
|
|
155
|
+
export function markedError(r, libc) {
|
|
156
|
+
if (isOrderRead(r) && libc) {
|
|
157
|
+
return { code: "0A000", message: "a jsonb compared or sorted here holds a string Mason cannot order as the database's collation does (a combining mark, a Thai or " +
|
|
158
|
+
"Lao vowel written before its consonant, a private-use or unassigned code point), in a row written since its stored keys were counted; asked again " +
|
|
159
|
+
"in a minute, the error names the character (Mason gap libc-uncovered)" };
|
|
160
|
+
}
|
|
161
|
+
return { code: "XX000", message: `a stored value of ${pathText(r)} could not be computed when its row was written (a Mason bug: report it, with mason logs); ` +
|
|
162
|
+
"asked again in a minute, it is computed from the row's document" };
|
|
163
|
+
}
|
|
164
|
+
/** the functions that raise `markedError` at a marked row (src/duck/functions.ts): of the column read (a key as its hex,
|
|
165
|
+
* so neither is a constant DuckDB would fold), the code and the message; a key's answers a BLOB, a path's a VARCHAR */
|
|
166
|
+
export const MARKED_FUNCTIONS = { key: "__mason_marked_key", value: "__mason_marked_value" };
|
|
87
167
|
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
88
168
|
const kindOf = (n) => isObject(n) && Object.keys(n).length === 1 && /^[A-Z]/.test(Object.keys(n)[0]) ? Object.keys(n)[0] : null;
|
|
89
169
|
const bodyOf = (n) => n[kindOf(n)];
|