@mason-data/cli 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +101 -73
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +368 -25
  5. package/dist/src/api/postgres/encode.js +3 -1
  6. package/dist/src/api/postgres/explain.js +3 -2
  7. package/dist/src/api/postgres/listener.js +11 -3
  8. package/dist/src/api/postgres/protocol.js +39 -23
  9. package/dist/src/api/postgres/proxy.js +283 -0
  10. package/dist/src/api/postgres/schema.js +6 -0
  11. package/dist/src/api/postgres/server.js +1617 -245
  12. package/dist/src/api/postgres/translate.js +127 -10
  13. package/dist/src/api/request-log.js +18 -5
  14. package/dist/src/cdc/poll.js +5 -5
  15. package/dist/src/cli/background.js +47 -25
  16. package/dist/src/cli/bridge-stdin.js +101 -0
  17. package/dist/src/cli/cancel.js +75 -0
  18. package/dist/src/cli/command.js +170 -11
  19. package/dist/src/cli/commands.js +804 -209
  20. package/dist/src/cli/control.js +59 -0
  21. package/dist/src/cli/credentials.js +182 -0
  22. package/dist/src/cli/demo.js +20 -11
  23. package/dist/src/cli/deploy.js +574 -0
  24. package/dist/src/cli/dev.js +491 -130
  25. package/dist/src/cli/doctor.js +131 -38
  26. package/dist/src/cli/generated-files.js +143 -0
  27. package/dist/src/cli/guides.js +175 -36
  28. package/dist/src/cli/hosted.js +850 -0
  29. package/dist/src/cli/init.js +162 -65
  30. package/dist/src/cli/login.js +289 -0
  31. package/dist/src/cli/machine.js +13 -12
  32. package/dist/src/cli/main.js +11 -5
  33. package/dist/src/cli/mason.cjs +17 -0
  34. package/dist/src/cli/mcp.js +31 -22
  35. package/dist/src/cli/metrics.js +79 -0
  36. package/dist/src/cli/output-gate.js +176 -0
  37. package/dist/src/cli/page-share.js +96 -0
  38. package/dist/src/cli/pg-tls.js +14 -4
  39. package/dist/src/cli/pg-url.js +4 -0
  40. package/dist/src/cli/pin.js +19 -14
  41. package/dist/src/cli/pool-size.cjs +85 -0
  42. package/dist/src/cli/ports.js +2 -0
  43. package/dist/src/cli/preflight.js +138 -46
  44. package/dist/src/cli/project.js +142 -4
  45. package/dist/src/cli/readiness.js +76 -6
  46. package/dist/src/cli/registry.js +65 -38
  47. package/dist/src/cli/replica-hosted.js +665 -0
  48. package/dist/src/cli/replica.js +76 -0
  49. package/dist/src/cli/replication-checks.js +7 -5
  50. package/dist/src/cli/replication-mode.js +105 -26
  51. package/dist/src/cli/request-filter.js +134 -0
  52. package/dist/src/cli/roles.js +585 -0
  53. package/dist/src/cli/run-server.js +637 -48
  54. package/dist/src/cli/runs.js +1 -1
  55. package/dist/src/cli/security.js +67 -18
  56. package/dist/src/cli/serve.js +204 -46
  57. package/dist/src/cli/share-run.js +329 -0
  58. package/dist/src/cli/share.js +286 -0
  59. package/dist/src/cli/source-edit.js +75 -17
  60. package/dist/src/cli/source-role.js +344 -0
  61. package/dist/src/cli/sources.js +306 -47
  62. package/dist/src/cli/style.js +2 -1
  63. package/dist/src/cli/supervise.js +27 -24
  64. package/dist/src/cli/sync.js +55 -25
  65. package/dist/src/cli/token-hosted.js +337 -0
  66. package/dist/src/cli/tunnel.js +200 -0
  67. package/dist/src/cli/ui-ssh.js +460 -0
  68. package/dist/src/cli/ui.js +124 -17
  69. package/dist/src/cli/usage.js +128 -0
  70. package/dist/src/cli/user-hosted.js +299 -0
  71. package/dist/src/cli/user.js +573 -65
  72. package/dist/src/cli/version.js +9 -7
  73. package/dist/src/demo/seed.js +29 -0
  74. package/dist/src/duck/analyze.js +36 -20
  75. package/dist/src/duck/cancel.js +10 -1
  76. package/dist/src/duck/collation.js +266 -0
  77. package/dist/src/duck/constant-key.js +94 -0
  78. package/dist/src/duck/distinct-key.js +283 -26
  79. package/dist/src/duck/events.js +165 -0
  80. package/dist/src/duck/executor-worker.js +20 -11
  81. package/dist/src/duck/executor.js +47 -33
  82. package/dist/src/duck/explain.js +60 -46
  83. package/dist/src/duck/extensions.js +160 -0
  84. package/dist/src/duck/functions.js +461 -63
  85. package/dist/src/duck/gaps.js +48 -21
  86. package/dist/src/duck/generated-plan.js +1070 -0
  87. package/dist/src/duck/generated.js +906 -0
  88. package/dist/src/duck/grants.js +176 -0
  89. package/dist/src/duck/held-types.js +64 -0
  90. package/dist/src/duck/history.js +127 -0
  91. package/dist/src/duck/jsonb-order.js +9 -12
  92. package/dist/src/duck/kept-route.js +6 -5
  93. package/dist/src/duck/kept.js +15 -13
  94. package/dist/src/duck/late.js +1469 -40
  95. package/dist/src/duck/libc-bound.js +341 -0
  96. package/dist/src/duck/libc-order.js +892 -126
  97. package/dist/src/duck/pg-eval-worker.js +7 -1
  98. package/dist/src/duck/pg-eval.js +6 -4
  99. package/dist/src/duck/pgtree.js +774 -23
  100. package/dist/src/duck/plan.js +369 -47
  101. package/dist/src/duck/publication.js +20 -6
  102. package/dist/src/duck/requests.js +269 -93
  103. package/dist/src/duck/resolved.js +117 -2
  104. package/dist/src/duck/rewrite.js +4584 -580
  105. package/dist/src/duck/row-filter.js +655 -0
  106. package/dist/src/duck/statement.js +19 -0
  107. package/dist/src/duck/store.js +439 -177
  108. package/dist/src/duck/stream.js +19 -10
  109. package/dist/src/duck/sync.js +1411 -368
  110. package/dist/src/duck/token.js +4 -1
  111. package/dist/src/duck/types.js +69 -12
  112. package/dist/src/duck/usage.js +326 -0
  113. package/dist/src/duck/users.js +69 -15
  114. package/dist/src/duck/verified.js +161 -1
  115. package/dist/src/duck/views.js +5 -5
  116. package/dist/src/lib/database-name.js +62 -0
  117. package/dist/src/lib/database-url.js +55 -10
  118. package/dist/src/lib/dotenv.js +17 -4
  119. package/dist/src/lib/instant.js +50 -0
  120. package/dist/src/lib/log-buffer.js +77 -0
  121. package/dist/src/lib/log-read.js +196 -0
  122. package/dist/src/lib/pg-numeric.js +390 -49
  123. package/dist/src/lib/project-version.js +23 -23
  124. package/dist/src/lib/secret-mask.js +142 -0
  125. package/dist/src/lib/table-sync.js +282 -0
  126. package/dist/src/web/app.css +706 -66
  127. package/dist/src/web/app.js +331 -409
  128. package/dist/src/web/charts.js +229 -0
  129. package/dist/src/web/index.html +82 -18
  130. package/dist/src/web/pages/queries.js +192 -0
  131. package/dist/src/web/pages/status.js +611 -0
  132. package/dist/src/web/pages/syncs.js +155 -0
  133. package/dist/src/web/pages/tables.js +439 -0
  134. package/dist/src/web/pages/usage.js +397 -0
  135. package/dist/src/web/pages/users.js +219 -0
  136. package/dist/src/web/shared.js +584 -0
  137. package/dist/src/web/vendor/README.md +25 -0
  138. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  139. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  140. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  141. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  142. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  143. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  144. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  145. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  146. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  147. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  148. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  149. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  150. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  151. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  152. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  153. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  154. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  155. package/package.json +12 -4
@@ -1,12 +1,13 @@
1
1
  /**
2
- * The package's version when installed; else the image's (`MASON_VERSION`, stamped at build by images-publish.yml:
3
- * `v0.6.1` for a release, said `0.6.1` as the package says it, or `main-<sha>`); a checkout runs as "dev" (and
4
- * ClickHouse's `main` image).
2
+ * What this Mason is, and where that was read (what a person running it can act on follows from it: `mason serve`'s pin
3
+ * says the image to run, or the npm line): the package's version when installed; else the image's (`MASON_VERSION`,
4
+ * stamped at build by images-publish.yml: `v0.6.1` for a release, said `0.6.1` as the package says it, or `main-<sha>`);
5
+ * a checkout runs as "dev".
5
6
  */
6
7
  import * as fs from "node:fs";
7
8
  import * as path from "node:path";
8
9
  import { fileURLToPath } from "node:url";
9
- export function version() {
10
+ export function runningVersion() {
10
11
  // src/cli/ in a checkout (the repo's package.json, which is not this one), dist/src/cli/ in the package
11
12
  const here = path.dirname(fileURLToPath(import.meta.url));
12
13
  for (const p of [path.join(here, "..", "..", "package.json"), path.join(here, "..", "..", "..", "package.json")]) {
@@ -14,10 +15,11 @@ export function version() {
14
15
  continue;
15
16
  const pkg = JSON.parse(fs.readFileSync(p, "utf8"));
16
17
  if (pkg.name === "@mason-data/cli" && pkg.version)
17
- return pkg.version;
18
+ return { text: pkg.version, from: "package" };
18
19
  }
19
20
  const image = process.env.MASON_VERSION?.trim();
20
21
  if (image)
21
- return /^v\d+\.\d+\.\d+$/.test(image) ? image.slice(1) : image;
22
- return "dev";
22
+ return { text: /^v\d+\.\d+\.\d+$/.test(image) ? image.slice(1) : image, from: "image" };
23
+ return { text: "dev", from: "checkout" };
23
24
  }
25
+ export const version = () => runningVersion().text;
@@ -45,6 +45,35 @@ export const DEMO_TABLES = ["customers", "products", "orders", "order_items"];
45
45
  export const DEMO_TABLE_TO_ADD = "order_items";
46
46
  /** the tables `mason init demo` writes to the sources file: the database's, but the one left to add */
47
47
  export const DEMO_ADDED_TABLES = DEMO_TABLES.filter((t) => t !== DEMO_TABLE_TO_ADD);
48
+ /** how the demo's tables are kept current (the sources file's top `sync:`, src/lib/table-sync.ts): polled every 5 s,
49
+ * so the demo shows a copy keeping up with its writer — where a project `mason init` makes is `off` */
50
+ export const DEMO_SYNC = { mode: "poll", every: "5s", seconds: 5 };
51
+ /**
52
+ * THE GENERATED TABLE `mason init demo` WRITES (`mason/generated_tables/<file>`, src/cli/generated-files.ts reads it), so a
53
+ * person trying Mason meets a generated table without reading the docs: orders and revenue per day and status, kept
54
+ * current from the synced `orders`.
55
+ * - It reads `orders` ALONE, a table init adds (`DEMO_ADDED_TABLES`), never `DEMO_TABLE_TO_ADD`: it is built at the
56
+ * first start, and the person's first lesson (`mason add order_items`) stays the only one.
57
+ * - It NAMES ITS ZONE (`AT TIME ZONE 'UTC'`): a generated table has no session, so a form reading the zone is refused.
58
+ * The day is a `date`, the same in every client's TimeZone.
59
+ * - `revenue` is the sum of a `numeric(12,2)`: its scale is fixed by the type, so it is stored exactly.
60
+ * - `target_lag: 30s`: the writer adds an order every few seconds and the poll is 5 s, so it is seen re-running.
61
+ * Its name is the README's example's (`daily_orders.yml`), so the two read as one.
62
+ */
63
+ export const DEMO_GENERATED = {
64
+ file: "daily_orders.yml",
65
+ table: "daily_orders",
66
+ text: `# A generated table: a SELECT over the synced tables, kept current by Mason. Yours to commit; mason help generated.
67
+ kind: scheduled
68
+ target_lag: 30s # how far behind the synced orders it may be
69
+ sql: |
70
+ SELECT (created_at AT TIME ZONE 'UTC')::date AS day, status, count(*) AS orders, sum(total) AS revenue
71
+ FROM orders
72
+ GROUP BY 1, 2
73
+ order_by: day, status
74
+ comment: Orders and revenue per day (UTC) and status, kept current from the synced orders.
75
+ `,
76
+ };
48
77
  /** an order's status moves along this path (`changeSql`); `cancelled` is a side exit */
49
78
  export const ORDER_STATUSES = ["pending", "paid", "shipped", "delivered"];
50
79
  const ROWS_PER_INSERT = 1_000;
@@ -7,33 +7,36 @@
7
7
  * - the result's column NAMES, type OIDs and typmods are Postgres's RowDescription, and the parameters' types its
8
8
  * ParameterDescription;
9
9
  * - the analysed tree (`debug_print_parse`, src/duck/pgtree.ts) gives the rewrite every expression's resolved type.
10
- * The statement is analysed under the SESSION's search_path (an unqualified table is the one the session would read).
10
+ * The statement is analysed under the SESSION's state (src/api/postgres/catalog.ts sessionSetSql, the catalog's one
11
+ * applier): its search_path as written (an unqualified table is the one the session would read), its TimeZone and its
12
+ * DateStyle (a literal is folded under both: `'01/02/2026'::date` is the session's order).
11
13
  *
12
14
  * ONE BATCH, SERIALISED (the catalog's queue): the logging is switched on, the statement is parsed, described and
13
15
  * closed under a name of its own, and everything is set back, before the next session's call runs. Nothing of it is
14
16
  * left in the sidecar between calls — another session's Sync can lose nothing (tests/postgres-api/server.test.ts "a
15
17
  * catalog statement's portal, beside another session's").
16
18
  */
19
+ import { HELD } from "./held-types.js";
20
+ import { CATALOG_BASE_SQL, sessionSetSql } from "../api/postgres/catalog.js";
17
21
  import { cstr, decodeBackend, frame, int16, int32 } from "../api/postgres/protocol.js";
18
- import { parseTree, typesByLocation } from "./pgtree.js";
22
+ import { outputsByLocation, parseTree, typesByLocation } from "./pgtree.js";
19
23
  const NAME = "__mason_analysis";
20
- /** the catalog's TimeZone, set back after a statement set the session's (src/api/postgres/catalog.ts sets it at start) */
21
- const CATALOG_ZONE = "SET TimeZone = 'UTC'";
24
+ /** what a statement of no session is analysed under (a row filter's read, a unit test): UTC, under `public`, ISO with month-day-year */
25
+ const NO_SESSION = { timeZone: "UTC", searchPath: "public", dateStyle: "ISO, MDY" };
22
26
  const query = (sql) => frame("Q", cstr(sql));
23
27
  /** Asks the catalog Postgres to analyse `sql` (with these declared parameter OIDs, 0 = infer). */
24
- export async function analyze(run, sql, paramOids, searchPath = "public", timeZone = "UTC") {
28
+ export async function analyze(run, sql, paramOids, under = NO_SESSION) {
25
29
  const bytes = Buffer.concat([
26
- // the session's search_path, pg_catalog after it as the catalog's own setup has it (catalog.ts)
27
- // …and its TimeZone: Postgres parses a literal under it ('2026-01-01 12:00'::timestamptz is that zone's noon), and
28
- // the rewrite writes the value it parsed (src/duck/rewrite.ts `aConst`)
29
- query(`SET search_path = ${searchPath}, pg_catalog; SET TimeZone = '${timeZone.replace(/'/g, "''")}'; SET client_min_messages = log; SET debug_print_parse = on; SET debug_pretty_print = off`),
30
+ // the session's state: Postgres parses a literal under its zone and order ('2026-01-01 12:00'::timestamptz is that
31
+ // zone's noon), and the rewrite writes the value it parsed (src/duck/rewrite.ts `aConst`)
32
+ query(`${sessionSetSql(under)}; SET client_min_messages = log; SET debug_print_parse = on; SET debug_pretty_print = off`),
30
33
  frame("P", cstr(NAME), cstr(sql), int16(paramOids.length), ...paramOids.map(int32)),
31
34
  frame("D", Buffer.from("S"), cstr(NAME)),
32
35
  frame("C", Buffer.from("S"), cstr(NAME)),
33
36
  frame("S"),
34
- // the catalog's own TimeZone back (catalog.ts sets UTC at its start): RESET would be the server's boot default, the
35
- // machine's zone, under which the catalog then answered what reads no table
36
- query(`RESET debug_print_parse; RESET debug_pretty_print; RESET client_min_messages; ${CATALOG_ZONE}; SET search_path = public, pg_catalog`),
37
+ // the catalog's own state back (catalog.ts CATALOG_BASE): RESET would be the server's boot default, the machine's
38
+ // zone, under which the catalog then answered what reads no table
39
+ query(`RESET debug_print_parse; RESET debug_pretty_print; RESET client_min_messages; ${CATALOG_BASE_SQL}`),
37
40
  ]);
38
41
  const messages = decodeBackend(Buffer.from(await run(bytes)));
39
42
  // the replies to the first Query end at its ReadyForQuery; the statement's run to the next one
@@ -58,22 +61,26 @@ export async function analyze(run, sql, paramOids, searchPath = "public", timeZo
58
61
  params: pd?.type === "ParameterDescription" ? pd.oids : [],
59
62
  fields: rd?.type === "RowDescription" ? rd.fields.map((f) => ({ name: f.name, oid: f.typeOid, typmod: f.typeModifier })) : null,
60
63
  types: typesByLocation(tree),
64
+ outputs: outputsByLocation(tree),
61
65
  tree,
62
66
  };
63
67
  }
64
68
  /** the types whose input DuckDB reads otherwise than Postgres, as the catalog Postgres writes their value back */
65
69
  const PARAM_TYPES = {
66
70
  16: "boolean", 21: "smallint", 23: "integer", 20: "bigint", 700: "real", 701: "double precision", 1700: "numeric",
67
- 2950: "uuid", 114: "json", 3802: "jsonb", 1083: "time", 1082: "date", 1114: "timestamp", 1184: "timestamptz", 1186: "interval", 869: "inet",
71
+ 2950: "uuid", 114: "json", 3802: "jsonb", 1083: "time", 1082: "date", 1114: "timestamp", 1184: "timestamptz", 1186: "interval", 869: "inet", 1266: "timetz",
72
+ 1009: "text[]", 1015: "varchar[]",
73
+ // a type the copy holds as its text: the text Postgres writes the value as
74
+ ...Object.fromEntries(Object.entries(HELD).map(([name, h]) => [h.oid, name])),
68
75
  };
69
76
  /**
70
77
  * A statement's parameters as Postgres reads them: each value through its type's input function (the type Postgres
71
- * inferred, or the one declared), under the session's TimeZone — so `'1.5'` for a smallint is Postgres's 22P02, and
72
- * `'01/02/2026'` for a date is 2026-01-02. A value comes back as text DuckDB reads to the same value: a number, a
78
+ * inferred, or the one declared), under the session's state (its TimeZone and DateStyle) — so `'1.5'` for a smallint
79
+ * is Postgres's 22P02, and `'01/02/2026'` for a date is 2026-01-02 (2026-02-01 under `ISO, DMY`). A value comes back as text DuckDB reads to the same value: a number, a
73
80
  * boolean or a uuid canonical; a date as days and a timestamp as microseconds since 2000-01-01 (src/duck/rewrite.ts
74
81
  * `datumText` writes them), an interval as its months, days and microseconds; `infinity` as itself.
75
82
  */
76
- export async function readParameters(run, oids, values, timeZone) {
83
+ export async function readParameters(run, oids, values, under) {
77
84
  const quote = (s) => `'${s.replace(/'/g, "''")}'`;
78
85
  const items = values.map((v, i) => {
79
86
  const oid = oids[i] ?? 0, type = PARAM_TYPES[oid];
@@ -84,24 +91,33 @@ export async function readParameters(run, oids, values, timeZone) {
84
91
  return `CASE WHEN isfinite(${x}) THEN (${x} - date '2000-01-01')::text ELSE ${x}::text END`;
85
92
  if (oid === 1114 || oid === 1184)
86
93
  return `CASE WHEN isfinite(${x}) THEN (extract(epoch FROM ${x}) * 1000000 - 946684800000000)::int8::text ELSE ${x}::text END`;
94
+ // a text array as JSON, which holds what array_in read (a NULL element, any character) as DuckDB reads it back; a
95
+ // JSON array always starts with `[`, so a word in its place says why the value cannot be read as one list
96
+ // (TEXT_ARRAY_REFUSALS): several dimensions, or subscripts that do not start at 1, which a list of DuckDB's has not
97
+ if (oid === 1009 || oid === 1015)
98
+ return `CASE WHEN array_ndims(${x}) > 1 THEN 'multidim-array' WHEN array_lower(${x}, 1) <> 1 THEN 'array-lower-bound' ELSE to_json(${x})::text END`;
87
99
  if (oid === 1186)
88
100
  return `(extract(year FROM ${x}) * 12 + extract(month FROM ${x}))::text || ' ' || extract(day FROM ${x})::text || ' ' || ((extract(hour FROM ${x}) * 60 + extract(minute FROM ${x})) * 60000000 + extract(microseconds FROM ${x}))::int8::text`;
89
101
  return `${x}::text`;
90
102
  });
91
103
  if (items.every((x) => x === null))
92
104
  return { values: values.map(() => null) };
93
- const sql = `SET TimeZone = ${quote(timeZone)}; SELECT ${items.map((x) => x ?? "NULL").join(", ")}; ${CATALOG_ZONE}`;
105
+ const sql = `${sessionSetSql(under)}; SELECT ${items.map((x) => x ?? "NULL").join(", ")}; ${CATALOG_BASE_SQL}`;
94
106
  const messages = decodeBackend(Buffer.from(await run(query(sql))));
95
107
  const refused = messages.find((m) => m.type === "ErrorResponse");
96
108
  if (refused?.type === "ErrorResponse") {
97
- // the value's own error (22P02, 22003, 22008 …) — and the zone set back, which the failed statement skipped
98
- await run(query(CATALOG_ZONE));
109
+ // the value's own error (22P02, 22003, 22008 …) — and the catalog's state set back, which the failed statement skipped
110
+ await run(query(CATALOG_BASE_SQL));
99
111
  return { refused: refused.fields };
100
112
  }
101
113
  const row = messages.find((m) => m.type === "DataRow");
102
114
  if (row?.type !== "DataRow")
103
115
  throw new Error("the catalog Postgres read the parameters and answered no row: report it");
104
- return { values: items.map((x, i) => x === null ? null : { oid: oids[i], text: row.values[i].toString("utf8") }) };
116
+ const read = items.map((x, i) => x === null ? null : { oid: oids[i], text: row.values[i].toString("utf8") });
117
+ const unlisted = read.findIndex((r) => r !== null && (r.oid === 1009 || r.oid === 1015) && !r.text.startsWith("["));
118
+ if (unlisted >= 0)
119
+ return { gap: read[unlisted].text, parameter: unlisted + 1 };
120
+ return { values: read };
105
121
  }
106
122
  /** One read of the catalog Postgres's own catalog (a signature from pg_proc, a type's name): its rows as text. */
107
123
  export async function catalogRows(run, sql) {
@@ -12,13 +12,22 @@
12
12
  * running`: a cancel of a statement already past never reaches the next one, a cancel that lands before its statement
13
13
  * starts still does, and a statement being drained after its cancel stays cancelled whatever the next one's number.
14
14
  * Dependency-free: the listener's thread loads it without DuckDB.
15
+ *
16
+ * ONE CELL MORE, THE WORKER'S BEAT: a statement that calls a Mason function holds the worker's event loop for a thousand
17
+ * calls at a time (minutes over wide documents), so no message can tell whether its JavaScript runs. Every call, and every
18
+ * row of a call, adds to the cell, and the executor's watch reads it (src/duck/executor.ts SILENT_MS).
15
19
  */
16
20
  /** how many wire sessions may hold a DuckDB connection at once (a slot each) */
17
21
  export const CANCEL_SLOTS = 4096;
18
22
  /** a fresh table (the executor makes one and hands it to its worker) */
19
- export function cancelTable() { return new Int32Array(new SharedArrayBuffer(CANCEL_SLOTS * 2 * 4)); }
23
+ export function cancelTable() { return new Int32Array(new SharedArrayBuffer((CANCEL_SLOTS * 2 + 1) * 4)); }
20
24
  const cancelledAt = (slot) => slot * 2;
21
25
  const runningAt = (slot) => slot * 2 + 1;
26
+ const BEAT_AT = CANCEL_SLOTS * 2;
27
+ /** the worker's JavaScript is running a Mason function (the cell is only ever compared, so its wrap is nothing) */
28
+ export function beat(t) { Atomics.add(t, BEAT_AT, 1); }
29
+ /** the beat as it stands: what the watch compares with its last look */
30
+ export function beats(t) { return Atomics.load(t, BEAT_AT); }
22
31
  /** a slot handed to a new session: nothing cancelled, nothing running */
23
32
  export function resetSlot(t, slot) { Atomics.store(t, cancelledAt(slot), 0); Atomics.store(t, runningAt(slot), 0); }
24
33
  /** statement `seq` of the session in `slot` is cancelled (and every one before it): monotonic, from either thread */
@@ -0,0 +1,266 @@
1
+ /**
2
+ * WHAT A COLLATION MEANS, IN ONE PLACE (issue 253, decision B; docs/DESIGN.md "Postgres decides, DuckDB computes"). WHICH
3
+ * collation orders a text is Postgres's analysis: each analysed node carries the OID it resolved (src/duck/pgtree.ts
4
+ * `Typed.collation`, exprCollation, and `Typed.inputCollation`, the one an operator, a GREATEST, an aggregate or a
5
+ * window compared under), the explicit COLLATE, a column's own, a derived table's and a set operation's column alike,
6
+ * and Postgres's own 42P21 / 42P22 / 42704 come from that analysis. WHAT that OID means is this module alone:
7
+ *
8
+ * OID → the catalog's pg_collation row (its schema and name) → the SOURCE's pg_collation row of that name (read at the
9
+ * start, src/cli/preflight.ts `SourceFacts.collations`) → a `DuckCollation`: byte order, an ICU locale DuckDB has
10
+ * (through the ONE name mapper, `icuName`), or a libc order read from the source (src/duck/libc-order.ts), named by
11
+ * its collcollate so the engine keys each with its own order.
12
+ *
13
+ * The catalog declares the source's collations (src/api/postgres/catalog.ts `catalogDdl`), so a name the source has
14
+ * resolves there and one it lacks is Postgres's 42704 there; a collation PGlite has of its own that the source lacks is
15
+ * refused here with the same 42704, never answered as if the source had it. OID 100 is the database's collation, 0 none.
16
+ *
17
+ * REFUSED BY NAME, never another order (`collation-unknown`): a nondeterministic collation anywhere in a statement
18
+ * (Postgres equates and groups under it: no equality key of the engine's is its), an ICU locale the engine cannot
19
+ * follow exactly where it orders (a `-u-` keyword such as `und-u-kn`; a base language the engine has no collation
20
+ * for), and a libc order that could not be read (`libc-locale-unread`).
21
+ *
22
+ * CASE MAPPING (issue 406) is the other fact a collation decides: upper, lower and initcap map under the call's input
23
+ * collation (`Typed.inputCollation`), as that collation's CTYPE says (`caseMappingOf`: a libc collctype or the
24
+ * database's datctype, an ICU or builtin locale), asked through `StatementCollations.caseOf`, apart from the order's
25
+ * refusals. Refused by name (`case-mapping-unknown`): a Turkish or Azeri libc ctype, a builtin locale Mason does not know.
26
+ */
27
+ import { Unrewritable } from "./rewrite.js";
28
+ /** An ICU locale as the case mapping names it: its BCP 47 tag (`de_AT` is `de-AT`, ICU's `@` keywords dropped), `und`
29
+ * for the root's and for one that is no tag. ICU's casing reads the tag's language alone (`tr`'s `i`, `nl`'s `IJ`);
30
+ * initcap's word break reads the whole tag (`en-US-u-va-posix` breaks a word at a full stop) */
31
+ export function icuLocale(locale) {
32
+ const tag = locale.replace(/[.@].*$/, "").replace(/_/g, "-");
33
+ if (!/^[A-Za-z]{2,3}([-][A-Za-z0-9]{1,8})*$/.test(tag))
34
+ return "und";
35
+ // (a tag of that shape the runtime still finds invalid, such as a repeated variant, is Intl's RangeError: its
36
+ // language alone then, which is all ICU's casing reads)
37
+ try {
38
+ return Intl.getCanonicalLocales(tag)[0] ?? "und";
39
+ }
40
+ catch (e) {
41
+ if (e instanceof RangeError)
42
+ return tag.split("-")[0].toLowerCase();
43
+ throw e;
44
+ }
45
+ }
46
+ /** the C or POSIX ctype exactly (Postgres's lc_ctype_is_c): `C.UTF-8` is glibc's own locale, which maps every letter */
47
+ const C_CTYPE = /^(C|POSIX)$/i;
48
+ /** a libc ctype whose towupper is not glibc's shared i18n one: Turkish's and Azeri's dotted and dotless i */
49
+ const TURKIC_CTYPE = /^(tr|az)([_.@-]|$)/i;
50
+ /**
51
+ * THE CASE MAPPING OF A COLLATION, from what decides it in Postgres: the CTYPE side of its locale (`ctype`: a libc
52
+ * one's collctype, the database's datctype), or its ICU or builtin locale (`locale`). Null where Mason cannot follow it:
53
+ * a Turkish or Azeri libc ctype, a builtin locale it does not know. Never the collation's ORDER (its collcollate): a
54
+ * database of datcollate C and datctype en_US.UTF-8 maps case as en_US.
55
+ */
56
+ export function caseMappingOf(provider, ctype, locale) {
57
+ if (provider === "i")
58
+ return `icu:${icuLocale(locale ?? ctype ?? "")}`;
59
+ if (provider === "b") {
60
+ const l = (locale ?? "").toUpperCase();
61
+ return l === "C" ? "ascii" : l === "C.UTF-8" || l === "C.UTF8" ? "simple" : l === "PG_UNICODE_FAST" ? "full" : null;
62
+ }
63
+ const c = ctype ?? "C";
64
+ if (C_CTYPE.test(c))
65
+ return "ascii";
66
+ return TURKIC_CTYPE.test(c) ? null : "libc";
67
+ }
68
+ const BYTE_LOCALE = /^(C|POSIX)([.].*)?$/i;
69
+ /** Whether a libc locale is one whose order is read from the source (not C or POSIX) */
70
+ export const isLibcCollation = (provider, collate) => provider !== "b" && provider !== "i" && provider !== "d" && !BYTE_LOCALE.test(collate);
71
+ /** THE ONE ICU NAME MAPPER: an ICU locale (`en-US`, `und`, `de_AT`) as the engine's collation name, its base language
72
+ * where the engine lacks the region; null where it has neither. Postgres's `und` (the root locale) is the engine's
73
+ * `en_us`, as `unicode` is. */
74
+ export function icuName(locale, known) {
75
+ const lang = locale.replace(/[.@].*$/, "").replace(/-/g, "_").toLowerCase();
76
+ if (lang === "und" || lang === "root" || lang === "")
77
+ return known.has("en_us") ? "en_us" : null;
78
+ if (known.has(lang))
79
+ return lang;
80
+ const base = lang.split("_")[0];
81
+ return known.has(base) ? base : null;
82
+ }
83
+ /** The source DATABASE's collation as the engine follows it, from pg_database: `provider` c (libc), i (ICU) or b (builtin). */
84
+ export function duckCollation(provider, collate, locale, known) {
85
+ if (provider === "b")
86
+ return { kind: "bytes" };
87
+ if (BYTE_LOCALE.test(collate) && provider !== "i")
88
+ return { kind: "bytes" };
89
+ if (provider === "i") {
90
+ const name = icuName(locale ?? collate, known);
91
+ if (name === null)
92
+ throw new Error(`the source database's ICU locale ${JSON.stringify(locale ?? collate)} has no collation in the engine: Mason cannot sort text as it does`);
93
+ return { kind: "icu", name };
94
+ }
95
+ return { kind: "libc", collate };
96
+ }
97
+ const refused = (message, hint) => new Unrewritable(message, hint, "collation-unknown");
98
+ /** the refusal of a case mapping Mason cannot follow, of the collation it names */
99
+ const caseRefused = (what, ctype) => new Unrewritable(`upper, lower and initcap under ${what} (its ctype ${JSON.stringify(ctype)}) are not supported by Mason: its case mapping is not one Mason follows (Turkish's and Azeri's i under libc, or a builtin locale Mason does not know)`, `write COLLATE "C" for ASCII letters only, or COLLATE "und-x-icu" for Unicode's mapping`, "case-mapping-unknown");
100
+ /**
101
+ * THE RESOLVER, built once per listener (src/duck/store.ts `listener`). `source` is null in the unit world (no source
102
+ * read): the catalog's own row is then the source's. `readLibc` reads a libc order of a collcollate the source has
103
+ * (src/cli/dev.ts: from the source, under a bound, kept in the synced data), on its first use.
104
+ */
105
+ export class CollationResolver {
106
+ database;
107
+ /** the database's case mapping (pg_database's datctype, or its ICU or builtin locale): null where Mason cannot follow it */
108
+ #databaseCase;
109
+ #databaseCollate;
110
+ #source;
111
+ #known;
112
+ #libc = new Map();
113
+ #reading = new Map();
114
+ #readLibc;
115
+ /** `databaseCollate`: the database's datcollate where its provider is libc (a libc collation of that locale is the
116
+ * database's order), else null. `databaseCase`: the database's case mapping (`caseMappingOf` its datctype or locale)
117
+ * and the ctype it was read from; absent (a unit test, a store with no source), glibc's, or ICU's root's where the
118
+ * database orders by ICU */
119
+ constructor(o) {
120
+ this.database = o.database;
121
+ this.#databaseCase = o.databaseCase ?? (o.database.kind === "icu" ? { mapping: "icu:und", ctype: "und" } : { mapping: "libc", ctype: "en_US.UTF-8" });
122
+ this.#databaseCollate = o.databaseCollate ?? null;
123
+ this.#source = o.source === undefined || o.source === null ? null : new Map(o.source.map((c) => [`${c.schema}\u0000${c.name}`, c]));
124
+ this.#known = o.known;
125
+ for (const [k, v] of o.libc ?? [])
126
+ this.#libc.set(k, v);
127
+ this.#readLibc = o.readLibc ?? null;
128
+ }
129
+ /** A resolver of the database's collation alone, for a caller with no catalog collations to name (a unit test's
130
+ * `planDuck`): every other OID is resolved from the catalog's own row. */
131
+ static of(database, known = new Set(["en_us", "en", "de", "fr", "sv", "tr", "da", "es", "ja", "zh"]), databaseCase) {
132
+ return new CollationResolver({ database, known, ...(databaseCase === undefined ? {} : { databaseCase }) });
133
+ }
134
+ /** the database's case mapping (OID 100), or its refusal */
135
+ #databaseMapping() {
136
+ if (this.#databaseCase.mapping === null)
137
+ throw caseRefused("the database's collation", this.#databaseCase.ctype);
138
+ return this.#databaseCase.mapping;
139
+ }
140
+ /** a libc order held (read at the start, or on first use), by collcollate */
141
+ libcOrder(collate) { return this.#libc.get(collate); }
142
+ /** the libc orders held, by collcollate */
143
+ libcOrders() { return this.#libc; }
144
+ /** Holds a libc order read elsewhere (the database's, at the start) */
145
+ hold(collate, order) { this.#libc.set(collate, order); }
146
+ /**
147
+ * ONE STATEMENT'S COLLATIONS: each OID its analysis uses (`oids`) read from the catalog once, the source's row of its
148
+ * name found, a nondeterministic one refused (anywhere: the statement may equate under it), and a libc order not yet
149
+ * held read before the plan is made. What `resolve` refuses is decided here and raised where it orders.
150
+ */
151
+ async prepare(rows, oids) {
152
+ const wanted = [...new Set(oids)].filter((o) => o !== 0 && o !== 100 && Number.isInteger(o)).sort((a, b) => a - b);
153
+ const meant = new Map();
154
+ const cased = new Map();
155
+ if (wanted.length > 0) {
156
+ const read = await rows(`SELECT c.oid::text, n.nspname, c.collname, c.collprovider::text, c.collcollate, c.collctype, c.colllocale, c.collisdeterministic::text ` +
157
+ `FROM pg_catalog.pg_collation c JOIN pg_catalog.pg_namespace n ON n.oid = c.collnamespace WHERE c.oid IN (${wanted.join(", ")})`);
158
+ for (const [oid, schema, name, provider, collate, ctype, locale, deterministic] of read) {
159
+ const own = { oid: Number(oid), schema: schema, name: name, provider: provider, collate: collate ?? null, ctype: ctype ?? null, locale: locale ?? null, deterministic: deterministic === "true" };
160
+ meant.set(own.oid, await this.#meaning(own));
161
+ cased.set(own.oid, this.#caseMeaning(own));
162
+ }
163
+ }
164
+ for (const o of wanted)
165
+ if (!meant.has(o))
166
+ throw new Error(`the collation ${o} of a statement's analysis is not in the catalog (Mason bug: report it)`);
167
+ return {
168
+ database: this.database,
169
+ resolve: (oid) => {
170
+ if (oid === 0)
171
+ return null;
172
+ if (oid === 100)
173
+ return this.database;
174
+ const m = meant.get(oid);
175
+ if (m === undefined)
176
+ throw new Error(`the collation ${oid} was not prepared for the statement (Mason bug: report it)`);
177
+ if (m instanceof Unrewritable)
178
+ throw m;
179
+ return m;
180
+ },
181
+ caseOf: (oid) => {
182
+ if (oid === 100)
183
+ return this.#databaseMapping();
184
+ const m = cased.get(oid);
185
+ if (m === undefined)
186
+ throw new Error(`the collation ${oid} was not prepared for the statement (Mason bug: report it)`);
187
+ if (m instanceof Unrewritable)
188
+ throw m;
189
+ return m;
190
+ },
191
+ };
192
+ }
193
+ /** one catalog collation's case mapping: its SOURCE row's ctype or locale (`#meaning` refused a collation the source
194
+ * lacks, and a nondeterministic one, before this is asked) */
195
+ #caseMeaning(own) {
196
+ const c = this.#sourceRow(own);
197
+ if (c.provider === "d")
198
+ return this.#databaseCase.mapping ?? caseRefused("the database's collation", this.#databaseCase.ctype);
199
+ const ctype = c.provider === "c" ? c.ctype ?? c.collate ?? "C" : c.locale ?? c.collate ?? "";
200
+ return caseMappingOf(c.provider, ctype, c.locale) ?? caseRefused(`the collation "${own.name.replaceAll('"', '""')}"`, ctype);
201
+ }
202
+ /** the source's row of a catalog collation (the catalog's own in the unit world); undefined where the source lacks it */
203
+ #sourceRow(own) {
204
+ return this.#source === null ? { ...own } : this.#source.get(`${own.schema}\u0000${own.name}`);
205
+ }
206
+ /** what one catalog collation means to the engine, or the refusal it raises where it orders */
207
+ async #meaning(own) {
208
+ const quoted = `"${own.name.replaceAll('"', '""')}"`;
209
+ const c = this.#sourceRow(own);
210
+ // a collation PGlite has that the source lacks: the source's own error
211
+ if (c === undefined)
212
+ throw new Unrewritable(`collation ${quoted} for encoding "UTF8" does not exist`, "", { postgres: "42704" });
213
+ if (!c.deterministic) {
214
+ throw refused(`the collation ${quoted} is nondeterministic: Mason cannot follow it (Postgres equates, groups and sorts strings it holds equal, which Mason's equality does not)`, `write COLLATE "C" for byte order, or a deterministic collation`);
215
+ }
216
+ if (c.provider === "d")
217
+ return this.database;
218
+ if (c.provider === "b")
219
+ return { kind: "bytes" };
220
+ if (c.provider === "c") {
221
+ const collate = c.collate ?? "C";
222
+ if (BYTE_LOCALE.test(collate))
223
+ return { kind: "bytes" };
224
+ // the database's own locale: its order, read at the start (bytes where the source's strcoll is)
225
+ if (collate === this.#databaseCollate)
226
+ return this.database;
227
+ return this.#libcOf(c, collate, quoted);
228
+ }
229
+ if (c.provider === "i") {
230
+ const locale = c.locale ?? c.collate ?? "";
231
+ // a keyword (`-u-kn`, `@colNumeric=yes`) changes the order in ways the engine's collation of the language does not
232
+ if (/-u-|-t-|@/i.test(locale)) {
233
+ return refused(`the collation ${quoted} (ICU locale ${JSON.stringify(locale)}) sets an ICU keyword Mason cannot follow`, `write COLLATE "C" for byte order, or COLLATE "und-x-icu"`);
234
+ }
235
+ const name = icuName(locale, this.#known);
236
+ if (name === null)
237
+ return refused(`the collation ${quoted} (ICU locale ${JSON.stringify(locale)}) is not one Mason can follow`, `write COLLATE "C" for byte order, or COLLATE "und-x-icu"`);
238
+ return { kind: "icu", name };
239
+ }
240
+ return refused(`the collation ${quoted} is not one Mason can follow`, `write COLLATE "C" for byte order, or leave the COLLATE out for the database's`);
241
+ }
242
+ /** a libc collation other than the database's: its order held, else read once (a statement waits for it) */
243
+ async #libcOf(c, collate, quoted) {
244
+ let order = this.#libc.get(collate);
245
+ if (order === undefined && this.#readLibc !== null) {
246
+ let reading = this.#reading.get(collate);
247
+ if (reading === undefined) {
248
+ reading = this.#readLibc(c);
249
+ this.#reading.set(collate, reading);
250
+ }
251
+ try {
252
+ order = await reading;
253
+ this.#libc.set(collate, order);
254
+ }
255
+ catch (e) {
256
+ this.#reading.delete(collate);
257
+ return new Unrewritable(`the order of the collation ${quoted} (${collate}) could not be read from the source: ${e.message}`, `write COLLATE "C" for byte order, or COLLATE "und-x-icu"`, "libc-locale-unread");
258
+ }
259
+ }
260
+ if (order === undefined)
261
+ return new Unrewritable(`the order of the collation ${quoted} (${collate}) was not read from the source`, `write COLLATE "C" for byte order, or COLLATE "und-x-icu"`, "libc-locale-unread");
262
+ if (order.bytes)
263
+ return { kind: "bytes" };
264
+ return { kind: "libc", collate };
265
+ }
266
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A CONSTANT SORT KEY (the class, issue 134: a fact derived in several places, whether a sort key is constant). A
3
+ * constant orders no row, and written into the engine's sort as a bare literal it is an ordinal (an integer) or refused
4
+ * (42804, a non-integer literal). Five findings on PR 170 were each a spelling a syntactic matcher took or missed
5
+ * (`1.5::numeric`, a Const to Postgres, written as the bare 1.5), so it is decided from Postgres's ANALYSIS, here alone:
6
+ * an expression is constant where the analysis put a Const (under a COLLATE, which changes no value). The analysis has
7
+ * folded a cast of an untyped literal and a cast to the type the value has (`'x'::text`, `NULL::int`, `1.5::numeric`)
8
+ * and checked them; anything else is a node Postgres evaluates (`1::bigint`, `2147483648::int`, `12345::numeric(3)`,
9
+ * `'abc'::text::int`, `1 / 0`, `$1`), at planning where it may raise, so it is a key the engine evaluates too.
10
+ *
11
+ * Read by `typesByLocation` (src/duck/pgtree.ts: `Typed.constant`, what src/duck/rewrite.ts drops a SELECT's key by,
12
+ * resolved through its output's name or number) and by src/duck/late.ts (a derived table's column, folded into the
13
+ * query around it).
14
+ */
15
+ export function constantExpression(v) {
16
+ if (v === null || v === undefined || typeof v !== "object" || Array.isArray(v))
17
+ return false;
18
+ return v._node === "CONST" || (v._node === "COLLATEEXPR" && constantExpression(v.arg));
19
+ }
20
+ /**
21
+ * WHETHER EACH KEY OF A CLAUSE IS CONSTANT, BY THE ENTRY POSTGRES BOUND IT TO (the class "an output not resolved to its
22
+ * column", its second instance: a key spelled unlike the output it equals, `'1.5'::numeric` beside `1.5::numeric AS v`,
23
+ * was matched by syntax, missed, and written bare). Postgres binds a key to an entry by its number or output name, or by
24
+ * EQUALITY OF THE ANALYSED EXPRESSIONS (findTargetlistEntrySQL99, the first equal entry), and only a key it bound to a
25
+ * new entry leaves a node at its own offset. So `known` is each key's entry where the caller can tell it (a number, an
26
+ * output name, an entry at the key's own offset) and null where Postgres bound it by equality; such a key is one of
27
+ * the clause's entries the analysis lists, and is resolved as the ways the clause reads that way: ORDER BY lists
28
+ * its keys' entries in order, each once (`sorted`); DISTINCT ON compares the set of them (`distinct`). What is known of
29
+ * such a key narrows them (`Unbound`). A key is constant when every way agrees it is; null where no way fits, or the
30
+ * ways disagree (the caller's own reading stands).
31
+ */
32
+ export function keysConstant(keys, clause, known) {
33
+ return keysBound(keys, clause, known).map((k) => k.constant);
34
+ }
35
+ /**
36
+ * EACH KEY'S ENTRY, AS POSTGRES BOUND IT (the class "an output not resolved to its column", its fourth round: the binding
37
+ * decides how a key is WRITTEN, not only whether it is constant). `entry` is the target entry every way the clause can
38
+ * read agrees the key is bound to (null where they disagree, or none fits); `constant` as `keysConstant`. A key bound to
39
+ * an entry below the outputs' count IS that output (src/duck/rewrite.ts `keysBound`, which writes it so).
40
+ */
41
+ export function keysBound(keys, clause, known) {
42
+ const listed = keys[clause];
43
+ const constant = (e) => keys.entries[e]?.constant ?? null;
44
+ const open = known.flatMap((k, i) => typeof k === "number" ? [] : [i]);
45
+ // a key bound by equality is bound to the FIRST entry equal to it, and one equal to it: alike in reading a column, and
46
+ // in being a Const where its own analysis is known
47
+ const first = [...new Set(listed.filter((e) => keys.entries[e]?.first === e))];
48
+ const choices = open.map((i) => {
49
+ const k = known[i];
50
+ return first.filter((e) => (k.reads === null || keys.entries[e].reads === k.reads) && (k.constant === null || keys.entries[e].constant === k.constant)
51
+ && (k.groupings === undefined || keys.entries[e].groupings.length === k.groupings));
52
+ });
53
+ const agreed = known.map((k) => typeof k === "number" ? { entry: k, constant: constant(k) } : { entry: null, constant: null });
54
+ if (open.length === 0)
55
+ return agreed;
56
+ const fits = (entries) => {
57
+ if (clause === "distinct")
58
+ return new Set(entries).size === new Set(listed).size && listed.every((e) => entries.includes(e));
59
+ const read = [];
60
+ for (const e of entries)
61
+ if (!read.includes(e))
62
+ read.push(e);
63
+ return read.length === listed.length && read.every((e, i) => e === listed[i]);
64
+ };
65
+ // every way the open keys can be bound (a clause has a handful of keys; past the bound, the caller's reading stands)
66
+ const ways = choices.reduce((n, c) => n * c.length, 1);
67
+ if (ways > 4096)
68
+ return agreed;
69
+ const seen = open.map(() => new Set());
70
+ const bound = open.map(() => new Set());
71
+ const choice = open.map(() => 0);
72
+ for (let n = 0; n < ways; n++) {
73
+ let at = n;
74
+ choices.forEach((c, i) => { choice[i] = c[at % c.length]; at = Math.floor(at / c.length); });
75
+ const entries = known.map((k, i) => typeof k === "number" ? k : choice[open.indexOf(i)]);
76
+ if (fits(entries))
77
+ open.forEach((_, i) => { seen[i].add(constant(choice[i])); bound[i].add(choice[i]); });
78
+ }
79
+ open.forEach((key, i) => {
80
+ agreed[key] = { entry: bound[i].size === 1 ? [...bound[i]][0] : null, constant: seen[i].size === 1 ? [...seen[i]][0] : null };
81
+ });
82
+ return agreed;
83
+ }
84
+ /**
85
+ * A KEY WRITTEN AS A BARE LITERAL that the analysis did not call constant (a rewrite that wrote a value it computed):
86
+ * the engine would read it as an ordinal or refuse it, so it is written as an expression the engine evaluates, which
87
+ * orders nothing, as the value does in Postgres. A literal cannot raise, and its text never fails a cast.
88
+ */
89
+ export function evaluated(n) {
90
+ const kind = typeof n === "object" && n !== null && !Array.isArray(n) ? Object.keys(n)[0] : undefined;
91
+ return kind === "A_Const"
92
+ ? { TypeCast: { arg: n, typeName: { names: [{ String: { sval: "varchar" } }], typemod: -1 } } }
93
+ : n;
94
+ }