@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.
Files changed (45) hide show
  1. package/README.md +71 -12
  2. package/dist/src/api/postgres/catalog.js +36 -1
  3. package/dist/src/api/postgres/server.js +133 -32
  4. package/dist/src/cdc/pg-wire.js +144 -38
  5. package/dist/src/cli/background.js +29 -5
  6. package/dist/src/cli/command.js +4 -3
  7. package/dist/src/cli/commands.js +259 -127
  8. package/dist/src/cli/demo.js +149 -0
  9. package/dist/src/cli/dev.js +113 -20
  10. package/dist/src/cli/doctor.js +6 -3
  11. package/dist/src/cli/guides.js +32 -8
  12. package/dist/src/cli/init.js +58 -18
  13. package/dist/src/cli/machine.js +8 -3
  14. package/dist/src/cli/main.js +2 -2
  15. package/dist/src/cli/mcp.js +726 -0
  16. package/dist/src/cli/preflight.js +16 -7
  17. package/dist/src/cli/project.js +5 -3
  18. package/dist/src/cli/registry.js +8 -5
  19. package/dist/src/cli/run-server.js +29 -106
  20. package/dist/src/cli/serve.js +6 -1
  21. package/dist/src/cli/source-edit.js +116 -0
  22. package/dist/src/cli/sources.js +6 -4
  23. package/dist/src/demo/seed.js +237 -0
  24. package/dist/src/demo/source.js +655 -0
  25. package/dist/src/duck/distinct-key.js +228 -0
  26. package/dist/src/duck/executor-worker.js +7 -2
  27. package/dist/src/duck/executor.js +67 -26
  28. package/dist/src/duck/explain.js +128 -9
  29. package/dist/src/duck/functions.js +60 -5
  30. package/dist/src/duck/gaps.js +9 -4
  31. package/dist/src/duck/jsonb-order.js +175 -0
  32. package/dist/src/duck/jsonb.js +10 -8
  33. package/dist/src/duck/kept-route.js +82 -2
  34. package/dist/src/duck/kept.js +206 -39
  35. package/dist/src/duck/pgtree.js +40 -0
  36. package/dist/src/duck/plan.js +21 -8
  37. package/dist/src/duck/rewrite.js +1168 -68
  38. package/dist/src/duck/store.js +132 -56
  39. package/dist/src/duck/sync.js +90 -25
  40. package/dist/src/duck/verified.js +6 -0
  41. package/dist/src/lib/timezone.js +3 -2
  42. package/dist/src/web/app.css +11 -0
  43. package/dist/src/web/app.js +167 -12
  44. package/dist/src/web/index.html +6 -1
  45. 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 x = root(parseJson(a)), y = root(parseJson(b));
450
+ const pa = parseJson(a), pb = parseJson(b);
451
+ const x = root(pa), y = root(pb);
452
+ // a raw scalar (stored as a one-element array) contains no array: `'1' @> '[1]'` is false, `'[1]' @> '1'` true
453
+ // (JsonbDeepContains refuses a raw scalar on the left of a real array)
454
+ if (pa.t !== "arr" && pa.t !== "obj" && pb.t === "arr")
455
+ return false;
449
456
  return (x.t === "obj") === (y.t === "obj") && contains(x, y);
450
457
  }
451
458
  /** `j ? key` on jsonb (jsonb_exists): an object's top-level key, or a string element of an array (a string scalar's own) */
@@ -482,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
- scalar(conn, "__mason_json_elements", [VARCHAR, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, asText]) => listValue(jsonElements(String(j), canonical === true, asText === true)));
651
- scalar(conn, "__mason_json_keys", [VARCHAR, BOOLEAN], LIST(VARCHAR), ([j, canonical]) => listValue(jsonMembers(String(j), canonical === true, false).map(([k]) => k)));
652
- scalar(conn, "__mason_json_values", [VARCHAR, BOOLEAN, BOOLEAN], LIST(VARCHAR), ([j, canonical, asText]) => listValue(jsonMembers(String(j), canonical === true, asText === true).map(([, v]) => v)));
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)));
@@ -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: "ORDER BY, GROUP BY, DISTINCT or <, >, = over jsonb (Postgres orders and equates jsonb by its own rules: numbers by value, objects by size first)", postgres: "answers", mason: "refused", wayOut: "compare a member: j ->> 'k', or j::text for the canonical text" },
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 select-list value that raises (`'x'::int`, a bad JSON number) in a row a LIMIT leaves out, where no index gives the ORDER BY", postgres: "raises: its plan sorts every row, its select list computed for each (through an index it stops at the limit and answers)", mason: "answers: a sorted, limited select list is computed for the rows the limit keeps (src/duck/late.ts), as Postgres's index scan does", wayOut: "none needed for an answer; filter the bad rows out to have neither raise" },
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 libm's", mason: "DuckDB's: equal to about 1e-15", wayOut: "round(x::numeric, 12)" },
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
+ }
@@ -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)];