@mason-data/cli 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +147 -69
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog.js +258 -14
  4. package/dist/src/api/postgres/encode.js +3 -1
  5. package/dist/src/api/postgres/explain.js +3 -2
  6. package/dist/src/api/postgres/protocol.js +39 -23
  7. package/dist/src/api/postgres/server.js +1290 -223
  8. package/dist/src/api/postgres/translate.js +81 -9
  9. package/dist/src/api/request-log.js +18 -5
  10. package/dist/src/cdc/pg-wire.js +144 -38
  11. package/dist/src/cdc/poll.js +5 -5
  12. package/dist/src/cli/background.js +55 -8
  13. package/dist/src/cli/cancel.js +75 -0
  14. package/dist/src/cli/command.js +5 -3
  15. package/dist/src/cli/commands.js +843 -272
  16. package/dist/src/cli/control.js +59 -0
  17. package/dist/src/cli/demo.js +158 -0
  18. package/dist/src/cli/dev.js +458 -125
  19. package/dist/src/cli/doctor.js +25 -12
  20. package/dist/src/cli/guides.js +103 -31
  21. package/dist/src/cli/init.js +159 -58
  22. package/dist/src/cli/machine.js +18 -12
  23. package/dist/src/cli/main.js +5 -5
  24. package/dist/src/cli/mcp.js +735 -0
  25. package/dist/src/cli/metrics.js +79 -0
  26. package/dist/src/cli/pg-tls.js +13 -3
  27. package/dist/src/cli/pg-url.js +4 -0
  28. package/dist/src/cli/pin.js +13 -12
  29. package/dist/src/cli/preflight.js +24 -14
  30. package/dist/src/cli/project.js +48 -7
  31. package/dist/src/cli/readiness.js +38 -4
  32. package/dist/src/cli/registry.js +57 -35
  33. package/dist/src/cli/replication-checks.js +7 -5
  34. package/dist/src/cli/replication-mode.js +105 -26
  35. package/dist/src/cli/request-filter.js +134 -0
  36. package/dist/src/cli/roles.js +584 -0
  37. package/dist/src/cli/run-server.js +498 -133
  38. package/dist/src/cli/runs.js +1 -1
  39. package/dist/src/cli/security.js +10 -8
  40. package/dist/src/cli/serve.js +147 -40
  41. package/dist/src/cli/share-run.js +286 -0
  42. package/dist/src/cli/share.js +286 -0
  43. package/dist/src/cli/source-edit.js +168 -0
  44. package/dist/src/cli/sources.js +290 -39
  45. package/dist/src/cli/supervise.js +12 -1
  46. package/dist/src/cli/sync.js +55 -25
  47. package/dist/src/cli/tunnel.js +200 -0
  48. package/dist/src/cli/ui.js +9 -6
  49. package/dist/src/cli/usage.js +128 -0
  50. package/dist/src/cli/user.js +496 -40
  51. package/dist/src/cli/version.js +9 -7
  52. package/dist/src/demo/seed.js +240 -0
  53. package/dist/src/demo/source.js +655 -0
  54. package/dist/src/duck/cancel.js +10 -1
  55. package/dist/src/duck/distinct-key.js +228 -0
  56. package/dist/src/duck/events.js +134 -0
  57. package/dist/src/duck/executor-worker.js +25 -11
  58. package/dist/src/duck/executor.js +107 -53
  59. package/dist/src/duck/explain.js +158 -29
  60. package/dist/src/duck/extensions.js +160 -0
  61. package/dist/src/duck/functions.js +73 -19
  62. package/dist/src/duck/gaps.js +25 -11
  63. package/dist/src/duck/grants.js +176 -0
  64. package/dist/src/duck/history.js +123 -0
  65. package/dist/src/duck/jsonb-order.js +175 -0
  66. package/dist/src/duck/jsonb.js +10 -8
  67. package/dist/src/duck/kept-route.js +82 -2
  68. package/dist/src/duck/kept.js +211 -44
  69. package/dist/src/duck/pgtree.js +40 -0
  70. package/dist/src/duck/plan.js +120 -30
  71. package/dist/src/duck/publication.js +6 -6
  72. package/dist/src/duck/requests.js +265 -93
  73. package/dist/src/duck/rewrite.js +1193 -73
  74. package/dist/src/duck/row-filter.js +649 -0
  75. package/dist/src/duck/store.js +271 -159
  76. package/dist/src/duck/stream.js +10 -5
  77. package/dist/src/duck/sync.js +907 -275
  78. package/dist/src/duck/types.js +6 -5
  79. package/dist/src/duck/usage.js +324 -0
  80. package/dist/src/duck/users.js +64 -15
  81. package/dist/src/duck/verified.js +9 -0
  82. package/dist/src/duck/views.js +2 -3
  83. package/dist/src/lib/database-name.js +62 -0
  84. package/dist/src/lib/dotenv.js +17 -4
  85. package/dist/src/lib/log-buffer.js +68 -0
  86. package/dist/src/lib/pg-numeric.js +5 -2
  87. package/dist/src/lib/project-version.js +23 -23
  88. package/dist/src/lib/table-sync.js +267 -0
  89. package/dist/src/lib/timezone.js +3 -2
  90. package/dist/src/web/app.css +706 -55
  91. package/dist/src/web/app.js +330 -258
  92. package/dist/src/web/charts.js +229 -0
  93. package/dist/src/web/index.html +84 -15
  94. package/dist/src/web/pages/queries.js +192 -0
  95. package/dist/src/web/pages/status.js +446 -0
  96. package/dist/src/web/pages/syncs.js +155 -0
  97. package/dist/src/web/pages/tables.js +434 -0
  98. package/dist/src/web/pages/usage.js +397 -0
  99. package/dist/src/web/pages/users.js +218 -0
  100. package/dist/src/web/shared.js +568 -0
  101. package/dist/src/web/vendor/README.md +25 -0
  102. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  103. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  104. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  105. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  106. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  107. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  108. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  109. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  110. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  111. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  112. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  113. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  114. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  115. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  116. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  117. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  118. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  119. package/package.json +10 -3
@@ -1,5 +1,9 @@
1
- import { MASON_SCHEMA } from "./store.js";
2
- import { canonicalRead, HIDDEN_PREFIX, HIDDEN_RE, hiddenName, pathKey, pathText, readOfKey, VISIBLE_CATALOG } from "./kept-route.js";
1
+ import { DUCKDB_VERSION, MASON_SCHEMA } from "./store.js";
2
+ import { createHash } from "node:crypto";
3
+ import { canonicalRead, EQUAL_STEPS, HIDDEN_PREFIX, HIDDEN_RE, hiddenName, isEqualRead, isKeyRead, isOrderRead, KEY_NULL_HEX, KEY_RECOMPUTE_HEX, knownSteps, ORDER_STEPS, pathKey, pathText, readOfKey, VISIBLE_CATALOG } from "./kept-route.js";
4
+ import { JSONB_KEY_FUNCTIONS, KEY_VERSION, libcUncovered, STORED_SUFFIX } from "./jsonb-order.js";
5
+ import { encodeLibcOrder } from "./libc-order.js";
6
+ import { icuNameOf } from "./rewrite.js";
3
7
  /** a path read by this many statements in the request log is stored (a larger table's also needs `KEEP_FILL_SHARE`) */
4
8
  export const KEEP_AFTER_STATEMENTS = 3;
5
9
  /** a path of a SMALL table (`SMALL_FILL_SECONDS`) read by one statement that took this long (ms, the request log's
@@ -57,6 +61,8 @@ const MEASURE_AGAIN_MS = 10 * 60_000;
57
61
  export const BACKFILL_ROWS = 50_000;
58
62
  /** how often the request log is read for new paths, and backfills continued */
59
63
  export const REVIEW_MS = 10_000;
64
+ /** the marked rows whose documents are read to say what they are (`markedText`) */
65
+ const MARKS_READ = 10_000;
60
66
  /** every this many reviews, each READY column's unset rows are counted (the detector) */
61
67
  const CHECK_EVERY = 6;
62
68
  export const PATHS_TABLE = `${MASON_SCHEMA}.json_paths`;
@@ -83,7 +89,7 @@ export function storeJsonPathsOf(env) {
83
89
  if (v === "off")
84
90
  return false;
85
91
  throw new Error(`${STORE_JSON_PATHS_SETTING}=${JSON.stringify(env[STORE_JSON_PATHS_SETTING])} is not a setting: on (the default) stores the JSON paths statements keep reading ` +
86
- "as columns of the copy, off stores none (and drops those stored, at this start)");
92
+ "as columns of the synced table, off stores none (and drops those stored, at this start)");
87
93
  }
88
94
  const ident = (name) => `"${name.replaceAll('"', '""')}"`;
89
95
  const literal = (s) => `'${s.replaceAll("'", "''")}'`;
@@ -93,6 +99,50 @@ const tableOf = (source) => { const at = source.indexOf("."); return `${ident(so
93
99
  export function keptValueSql(p, value) {
94
100
  return `coalesce(__mason_json_kept(NULL, ${value}, ${literal(p.steps)}, ${p.either}, ${p.canonical}, false), ${NULL_SQL})`;
95
101
  }
102
+ /**
103
+ * A JSONB'S STORED KEY, from its column's `value` (src/duck/kept-route.ts `ORDER_STEPS`): the key the statement would
104
+ * compute (src/duck/rewrite.ts `jsonbKeyCall`), by the same functions (src/duck/jsonb-order.ts `JSONB_KEY_FUNCTIONS`,
105
+ * their stored twins), never NULL — `KEY_NULL_HEX` for a NULL document (the column's NULL is a row not stored yet), and
106
+ * `KEY_RECOMPUTE_HEX` where the key raised, which its read computes (and raises) again.
107
+ */
108
+ export function orderKeySql(c, value) {
109
+ const call = (name, ...args) => `${name}${STORED_SUFFIX}(NULL, ${args.join(", ")})`;
110
+ const key = c.kind === "bytes" ? call(JSONB_KEY_FUNCTIONS.bytes.key, value)
111
+ : call(JSONB_KEY_FUNCTIONS[c.kind].key, value, `list_transform(${call(JSONB_KEY_FUNCTIONS[c.kind].strings, value)}, __mason_s -> icu_sort_key(__mason_s, ${literal(icuNameOf(c))}))`);
112
+ return `CASE WHEN (${value}) IS NULL THEN unhex('${KEY_NULL_HEX}') ELSE coalesce(${key}, unhex('${KEY_RECOMPUTE_HEX}')) END`;
113
+ }
114
+ /**
115
+ * A STORED KEY'S ID in its `steps` (`ORDER_STEPS:<id>`): what its bytes depend on, so a key is read only where the
116
+ * statement's own key would be the same bytes — its collation (a libc one by its measured order's content: a glibc
117
+ * whose order moved is another collation), the key's encoding (`KEY_VERSION`) and the DuckDB whose ICU made its strings'
118
+ * keys (`DUCKDB_VERSION`: an upgrade may bring another ICU). Another id is another column, dropped at `orderBy`.
119
+ */
120
+ export function orderId(c, libc, versions = `k${KEY_VERSION}:${DUCKDB_VERSION}`) {
121
+ if (c.kind === "bytes")
122
+ return `c:${versions}`;
123
+ if (c.kind === "icu")
124
+ return `icu:${c.name}:${versions}`;
125
+ if (libc === undefined)
126
+ throw new Error("a libc collation's order was not read (Mason bug: report it)");
127
+ return `libc:${c.icu}:${createHash("sha256").update(encodeLibcOrder(libc)).digest("hex").slice(0, 16)}:${versions}`;
128
+ }
129
+ /**
130
+ * THE C KEY'S ID, stored under a libc collation for equality (`EQUAL_STEPS:<id>`, src/duck/kept-route.ts): the C
131
+ * collation's order id (`orderId`), so with the key's encoding (`KEY_VERSION`) and the DuckDB (`DUCKDB_VERSION`) in it,
132
+ * as an order key's — a release that changes either stores it again. The libc order is not in it: the C key reads no
133
+ * collation, so a glibc whose order moved keeps it.
134
+ */
135
+ export function equalId(versions) {
136
+ return versions === undefined ? orderId({ kind: "bytes" }, undefined) : orderId({ kind: "bytes" }, undefined, versions);
137
+ }
138
+ /** a stored column's type, and its mark of a value computed at read time, as SQL */
139
+ const columnType = (p) => isKeyRead(p) ? "BLOB" : "VARCHAR";
140
+ const recomputeSql = (p) => isKeyRead(p) ? `unhex('${KEY_RECOMPUTE_HEX}')` : RECOMPUTE_SQL;
141
+ /** what a fill's measured rate is kept by (`estimate`, `lastRate`): the JSON column, and whether the fill computes a path,
142
+ * a jsonb's order key or its C key — a key's function is not a path's (a list of strings, their ICU keys, the key's
143
+ * bytes), nor a libc order's key the C key's (4.7 µs a row against 1.4, docs/DESIGN.md "jsonb's order"), so one's
144
+ * rate is no evidence for another's */
145
+ const measuredKey = (r) => `${r.source}\u0000${r.column}\u0000${isOrderRead(r) ? "order" : isEqualRead(r) ? "equal" : "path"}`;
96
146
  /** a read as its stored path's `->` read (src/duck/kept-route.ts pathKey): what the ledger holds */
97
147
  export const pathOf = (r) => ({ ...canonicalRead(r), asText: false });
98
148
  /** a span of seconds as a person reads it: `4 s`, `9 min 40 s`, `1 h 28 min` */
@@ -180,14 +230,73 @@ export class KeptJsonPaths {
180
230
  }
181
231
  keepNow;
182
232
  // --------------------------------------------------------------------------------------------- what a plan reads
183
- ready(read) {
233
+ ready(asked) {
184
234
  if (!this.enabled)
185
235
  return null;
236
+ const read = this.own(asked);
186
237
  const p = this.paths.get(hiddenName(read));
187
238
  return p !== undefined && p.state === "ready" && pathKey(p) === pathKey(read) ? { name: p.name, exact: p.marked === 0 } : null;
188
239
  }
189
240
  visible() { return this.views; }
190
241
  generation() { return this.gen; }
242
+ // --------------------------------------------------------------------------------------------- a jsonb's order
243
+ /** the collation a jsonb's key is stored under (`orderBy`), and its id (`orderId`); null until told */
244
+ order = null;
245
+ /**
246
+ * A READ AS THIS KEEPER STORES IT: a jsonb's order (`ORDER_STEPS`) under the collation it was told; its equality
247
+ * (`EQUAL_STEPS`) as the C key (`equalId`) under a libc collation, and under C or ICU as the ORDER's read — the order's
248
+ * key equates there, so one column serves both and no second is ever made; any other read as it is.
249
+ */
250
+ own(r) {
251
+ if (isEqualRead(r) && this.order !== null && this.order.collation.kind !== "libc")
252
+ return { ...r, steps: `${ORDER_STEPS}:${this.order.id}` };
253
+ if (isEqualRead(r))
254
+ return { ...r, steps: `${EQUAL_STEPS}:${this.order === null ? "?" : equalId()}` };
255
+ return isOrderRead(r) ? { ...r, steps: `${ORDER_STEPS}:${this.order?.id ?? "?"}` } : r;
256
+ }
257
+ /** the steps a stored key would have now (`orderBy`): the order's under the collation told, the C key's under a libc
258
+ * one (none under C or ICU: the order's key serves); null until told */
259
+ nowSteps(p) {
260
+ if (this.order === null)
261
+ return null;
262
+ if (isEqualRead(p))
263
+ return this.order.collation.kind === "libc" ? `${EQUAL_STEPS}:${equalId()}` : null;
264
+ return `${ORDER_STEPS}:${this.order.id}`;
265
+ }
266
+ /** a stored key of the collation statements sort and equate by now (`orderBy`): computed by writes and read by plans */
267
+ current(p) { const now = this.nowSteps(p); return now !== null && p.steps === now; }
268
+ /**
269
+ * THE COLLATION, TOLD (src/duck/store.ts `listener`, once the source's is known): a jsonb's stored key is that
270
+ * collation's. Until then a stored key is written unset and read by no plan (`open`); now the keys of another
271
+ * collation (the source's changed, or its glibc's order moved) are dropped, and those of this one filled where a
272
+ * write left them unset — the backfill's sweep — and ready again.
273
+ */
274
+ async orderBy(collation, libc) {
275
+ if (!this.enabled)
276
+ return;
277
+ const id = orderId(collation, libc);
278
+ await this.host.onWriter(async () => {
279
+ this.order = { id, collation, libc };
280
+ for (const p of [...this.paths.values()].filter((x) => isKeyRead(x) && !this.current(x) && (x.state === "backfilling" || x.state === "ready"))) {
281
+ const was = p.steps.slice(p.steps.indexOf(":") + 1), now = this.nowSteps(p);
282
+ await this.drop(this.store.writer, p, now === null
283
+ ? `the database's collation is no longer a libc one (now ${id}): its order's key equates jsonb, so equality reads that one`
284
+ : `stored under another collation or version (${was}, now ${now.slice(now.indexOf(":") + 1)}); stored again when its reads ask for it`);
285
+ this.host.log(`${p.source}: ${pathText(p)} is no longer stored: ${p.why}`);
286
+ }
287
+ });
288
+ void this.fills();
289
+ }
290
+ /** what a stored column takes from its JSON column's `value`: a path's `->` answer (`keptValueSql`), a jsonb's order
291
+ * key (`orderKeySql` under the collation told), or its C key (`orderKeySql` under C) — unset (NULL) while the key is
292
+ * not the one told (`orderBy`): the sweep fills it */
293
+ valueSql(p, value) {
294
+ if (!isKeyRead(p))
295
+ return keptValueSql(p, value);
296
+ if (!this.current(p))
297
+ return "NULL";
298
+ return orderKeySql(isEqualRead(p) ? { kind: "bytes" } : this.order.collation, value);
299
+ }
191
300
  /** every path with a row, for `mason status` */
192
301
  status() {
193
302
  return [...this.paths.values()].sort((a, b) => a.source.localeCompare(b.source) || pathText(a).localeCompare(pathText(b)))
@@ -198,11 +307,11 @@ export class KeptJsonPaths {
198
307
  * write of whole rows names after the copy's own columns */
199
308
  stored(source, valueOf) {
200
309
  const kept = this.physical(source);
201
- return { names: kept.map((p) => p.name), values: kept.map((p) => keptValueSql(p, valueOf(p.column))) };
310
+ return { names: kept.map((p) => p.name), values: kept.map((p) => this.valueSql(p, valueOf(p.column))) };
202
311
  }
203
312
  /** what an UPDATE that sets `carried` sets too: each stored column read from one of them, from its new value */
204
313
  storedSet(source, carried, valueOf) {
205
- return this.physical(source).filter((p) => carried.includes(p.column)).map((p) => `${ident(p.name)} = ${keptValueSql(p, valueOf(p.column))}`);
314
+ return this.physical(source).filter((p) => carried.includes(p.column)).map((p) => `${ident(p.name)} = ${this.valueSql(p, valueOf(p.column))}`);
206
315
  }
207
316
  physical(source) {
208
317
  return [...this.paths.values()].filter((p) => p.source === source && (p.state === "backfilling" || p.state === "ready")).sort((a, b) => a.name.localeCompare(b.name));
@@ -230,6 +339,14 @@ export class KeptJsonPaths {
230
339
  basis: isBasis(fillBasis) ? fillBasis : "text", rowsPerSecond: fillRate === null || fillRate === undefined ? null : Number(fillRate) },
231
340
  filled: filledRows === null || filledRows === undefined || filledSeconds === null || filledSeconds === undefined ? null : { rows: Number(filledRows), seconds: Number(filledSeconds) } });
232
341
  }
342
+ // a row of a kind this Mason does not know (another release's: `knownSteps`) is dropped, its column with it (no
343
+ // write here could compute it, and none names it once dropped), and said with its steps: inert, never fatal
344
+ for (const p of this.paths.values()) {
345
+ if (p.state === "dropped" || knownSteps(p.steps))
346
+ continue;
347
+ await this.drop(w, p, `stored by another release as ${p.steps}, a kind this Mason does not know`);
348
+ this.host.log(`${p.source}: dropped the stored ${p.column} of a kind this Mason does not know (${p.steps}); its column goes at this start`);
349
+ }
233
350
  // a path an earlier Mason stored in another form (a column per spelling or per `->>`, its rows' marks other): its
234
351
  // name is not its path's (`hiddenName`), so it is dropped with its column, and stored again as the policy says
235
352
  const earlier = "stored by an earlier Mason in another form; stored again when its reads ask for it";
@@ -264,7 +381,15 @@ export class KeptJsonPaths {
264
381
  for (const p of this.paths.values()) {
265
382
  if (p.state === "dropped" || p.state === "waiting" || named.has(`${p.source}\u0000${p.name}`))
266
383
  continue;
267
- await this.drop(w, p, "its column is not in the copy");
384
+ await this.drop(w, p, "its column is not in the synced table");
385
+ }
386
+ // a jsonb's stored key is its collation's, told after this (`orderBy`): until then a write leaves it unset and no
387
+ // plan reads it, so a ready one is filled again where unset first — the backfill's sweep alone (its ranges are done)
388
+ for (const p of this.paths.values()) {
389
+ if (!isKeyRead(p) || p.state !== "ready")
390
+ continue;
391
+ p.state = "backfilling";
392
+ this.cursors.set(p.name, Number.MAX_SAFE_INTEGER);
268
393
  }
269
394
  await w.run(`ATTACH IF NOT EXISTS ':memory:' AS ${VISIBLE_CATALOG}`);
270
395
  for (const source of this.host.served().keys())
@@ -286,7 +411,9 @@ export class KeptJsonPaths {
286
411
  this.reader?.closeSync();
287
412
  this.reader = null;
288
413
  }
289
- /** a path's row marked dropped, with why (on the caller's connection and turn) */
414
+ /** a path's row marked dropped, with why (on the caller's connection and turn). Its COLUMN stays in the table until
415
+ * the next start, whose reconciling of the file with the ledger (`open`, "a column with no row") drops it: every
416
+ * write names only the stored columns (`stored`), and DuckDB's DROP COLUMN rewrites the table */
290
417
  async drop(c, p, why) {
291
418
  await c.run(`UPDATE ${PATHS_TABLE} SET state = 'dropped', why = ${literal(why)} WHERE name = ${literal(p.name)}`);
292
419
  if (p.state === "ready")
@@ -335,14 +462,14 @@ export class KeptJsonPaths {
335
462
  return;
336
463
  for (const p of this.physical(source).filter((x) => columns.includes(x.column))) {
337
464
  // no default: its NULL is "not stored yet", and an ADD COLUMN writes nothing into the rows (`keep`)
338
- await c.run(`ALTER TABLE ${side} ADD COLUMN IF NOT EXISTS ${ident(p.name)} VARCHAR`);
465
+ await c.run(`ALTER TABLE ${side} ADD COLUMN IF NOT EXISTS ${ident(p.name)} ${columnType(p)}`);
339
466
  const end = Number((await c.runAndReadAll(`SELECT coalesce(max(rowid), -1) + 1 FROM ${side}`)).getRowsJS()[0][0]);
340
467
  for (let lo = 0; lo < end; lo += this.backfillRows) {
341
- await run(`UPDATE ${side} SET ${ident(p.name)} = ${keptValueSql(p, ident(p.column))} WHERE rowid >= ${lo} AND rowid < ${lo + this.backfillRows} AND ${ident(p.name)} IS NULL`);
468
+ await run(`UPDATE ${side} SET ${ident(p.name)} = ${this.valueSql(p, ident(p.column))} WHERE rowid >= ${lo} AND rowid < ${lo + this.backfillRows} AND ${ident(p.name)} IS NULL`);
342
469
  }
343
470
  // rows the column cannot answer alone (KEPT_RECOMPUTE) turn its reads to the CASE before the side table serves;
344
471
  // counted again once it does (`swapped`)
345
- const marked = Number((await c.runAndReadAll(`SELECT count(*) FROM ${side} WHERE ${ident(p.name)} = ${RECOMPUTE_SQL}`)).getRowsJS()[0][0]);
472
+ const marked = Number((await c.runAndReadAll(`SELECT count(*) FROM ${side} WHERE ${ident(p.name)} = ${recomputeSql(p)}`)).getRowsJS()[0][0]);
346
473
  if (marked > p.marked) {
347
474
  if (p.marked === 0 && p.state === "ready")
348
475
  this.gen++;
@@ -355,7 +482,7 @@ export class KeptJsonPaths {
355
482
  async swapping(c, source, columns) {
356
483
  this.gone = [];
357
484
  for (const p of this.physical(source).filter((x) => !columns.includes(x.column))) {
358
- const why = `${p.column} is no longer a column of the copy of ${source}`;
485
+ const why = `${p.column} is no longer a column of the synced table ${source}`;
359
486
  await c.run(`UPDATE ${PATHS_TABLE} SET state = 'dropped', why = ${literal(why)} WHERE name = ${literal(p.name)}`);
360
487
  this.gone.push({ p, why });
361
488
  }
@@ -415,7 +542,7 @@ export class KeptJsonPaths {
415
542
  const read = readOfKey(key);
416
543
  if (read === null)
417
544
  continue;
418
- const path = pathOf(read), k = pathKey(path);
545
+ const path = this.own(pathOf(read)), k = pathKey(path);
419
546
  let e = by.get(k);
420
547
  if (e === undefined)
421
548
  by.set(k, e = { read: path, statements: new Map() });
@@ -436,14 +563,14 @@ export class KeptJsonPaths {
436
563
  async estimate(read) {
437
564
  if (this.estimateFor !== undefined)
438
565
  return this.estimateFor(read);
439
- const key = `${read.source}\u0000${read.column}`;
566
+ const key = measuredKey(read);
440
567
  const was = this.measured.get(key);
441
568
  if (was !== undefined && Date.now() - was.at < MEASURE_AGAIN_MS)
442
569
  return was.fill;
443
570
  this.reader ??= await this.store.instance.connect();
444
571
  const target = tableOf(read.source);
445
572
  const rows = Number((await this.reader.runAndReadAll(`SELECT count(*) FROM ${target}`)).getRowsJS()[0][0]);
446
- const rate = this.lastRate(read.source, read.column, rows);
573
+ const rate = this.lastRate(read, rows);
447
574
  let fill;
448
575
  if (rate !== null)
449
576
  fill = measuredEstimate(rows, rate);
@@ -460,12 +587,12 @@ export class KeptJsonPaths {
460
587
  this.measured.set(key, { at: Date.now(), fill });
461
588
  return fill;
462
589
  }
463
- /** the rows a second of the last fill of a JSON column (any path of it) that is evidence for a table of `rows` now
464
- * (`MEASURED_MIN_ROWS`, `MEASURED_SPAN`), or null */
465
- lastRate(source, column, rows) {
590
+ /** the rows a second of the last fill of a JSON column (any path of it; a jsonb's key by a key's fill alone) that is
591
+ * evidence for a table of `rows` now (`MEASURED_MIN_ROWS`, `MEASURED_SPAN`), or null */
592
+ lastRate(read, rows) {
466
593
  let last = null;
467
594
  for (const p of this.paths.values()) {
468
- if (p.source !== source || p.column !== column || p.filled === null || p.filled.seconds <= 0 || p.filled.rows < MEASURED_MIN_ROWS)
595
+ if (measuredKey(p) !== measuredKey(read) || p.filled === null || p.filled.seconds <= 0 || p.filled.rows < MEASURED_MIN_ROWS)
469
596
  continue;
470
597
  if (rows > MEASURED_SPAN * p.filled.rows)
471
598
  continue;
@@ -535,7 +662,7 @@ export class KeptJsonPaths {
535
662
  await this.check(p);
536
663
  }
537
664
  catch (e) {
538
- this.host.log(`stored JSON paths: the review failed, and runs again in ${Math.round(this.reviewMs / 1000)} s (the copies serve as they were): ${e instanceof Error ? e.message : String(e)}`);
665
+ this.host.log(`stored JSON paths: the review failed, and runs again in ${Math.round(this.reviewMs / 1000)} s (the synced tables serve as they were): ${e instanceof Error ? e.message : String(e)}`);
539
666
  }
540
667
  finally {
541
668
  this.reviewing = null;
@@ -593,11 +720,16 @@ export class KeptJsonPaths {
593
720
  const cols = this.host.served().get(read.source);
594
721
  if (cols === undefined || !this.views.has(read.source) || this.host.rebuilding(read.source))
595
722
  return false;
723
+ // a jsonb's key waits for its collation (`orderBy`, at the listener's start), and its C key is stored under a libc
724
+ // one alone (`own` folds it into the order's elsewhere): nothing to say
725
+ if (isKeyRead(read) && !this.current(read))
726
+ return false;
596
727
  const name = hiddenName(read);
597
728
  const col = cols.find((c) => c.name === read.column);
598
- const why = col === undefined ? `${read.column} is not a column of the copy`
729
+ const why = col === undefined ? `${read.column} is not a column of the synced table`
599
730
  : !/^jsonb?$/.test(col.type) ? `${read.column} is a ${col.type}, not a json or jsonb`
600
- : cols.some((c) => c.name.toLowerCase() === name) ? `the copy has a column named ${name} of its own` : null;
731
+ : isKeyRead(read) && col.type !== "jsonb" ? `${read.column} is a ${col.type}, which has no order or equality (a jsonb's is stored)`
732
+ : cols.some((c) => c.name.toLowerCase() === name) ? `the synced table has a column named ${name} of its own` : null;
601
733
  if (why === null)
602
734
  return true;
603
735
  const key = pathKey(read);
@@ -659,7 +791,7 @@ export class KeptJsonPaths {
659
791
  const target = tableOf(read.source);
660
792
  await w.run("BEGIN TRANSACTION");
661
793
  try {
662
- await w.run(`ALTER TABLE ${target} ADD COLUMN ${ident(name)} VARCHAR`);
794
+ await w.run(`ALTER TABLE ${target} ADD COLUMN ${ident(name)} ${columnType(read)}`);
663
795
  const rows = Number((await w.runAndReadAll(`SELECT count(*) FROM ${target}`)).getRowsJS()[0][0]);
664
796
  const p = { ...pathOf(read), name, state: "backfilling", rowsTotal: rows, rowsDone: 0, statements: c.statements, slowestMs: c.slowestMs, totalMs: c.totalMs,
665
797
  fill, filled: null, marked: 0, keptAt: new Date().toISOString(), readyAt: null, why };
@@ -682,6 +814,9 @@ export class KeptJsonPaths {
682
814
  /** a backfill continued to its end, a chunk a writer's turn (paused while its table is rebuilt: the next review goes
683
815
  * on), then the path made ready once no row of it is unset — and read by its column alone once none is marked */
684
816
  async backfill(p) {
817
+ // a jsonb's key waits for its collation (`orderBy`): its rows would be filled unset
818
+ if (isKeyRead(p) && !this.current(p))
819
+ return;
685
820
  const target = tableOf(p.source);
686
821
  const end = await this.host.onWriter(async () => Number((await this.store.writer.runAndReadAll(`SELECT coalesce(max(rowid), -1) + 1 FROM ${target}`)).getRowsJS()[0][0]));
687
822
  // what this fill measures: the rows its chunks filled and the time they held the writer (never the waits for its
@@ -700,28 +835,58 @@ export class KeptJsonPaths {
700
835
  if (!done)
701
836
  return;
702
837
  }
703
- await this.host.onWriter(async () => {
704
- if (this.stopped || this.host.rebuilding(p.source) || p.state !== "backfilling")
705
- return;
706
- // what the ranges did not reach (a row whose rowid moved), by the mark itself
707
- while (await this.fillChunk(p, `rowid IN (SELECT rowid FROM ${target} WHERE ${ident(p.name)} IS NULL LIMIT ${this.backfillRows}) AND `) > 0)
708
- ;
709
- p.marked = await this.markedRows(p);
710
- Object.assign(p, { state: "ready", readyAt: new Date().toISOString(), filled: rows > 0 && ms > 0 ? { rows, seconds: ms / 1000 } : p.filled });
711
- await this.record(this.store.writer, p);
712
- this.cursors.delete(p.name);
713
- this.measured.delete(`${p.source}\u0000${p.column}`);
714
- this.gen++;
715
- });
838
+ // THE SWEEP: what the ranges did not reach (a row whose rowid moved, a row a write left unset while a jsonb's
839
+ // collation was not told), by the mark itself — a chunk a writer's turn, as the ranges are; the turn whose chunk
840
+ // finds none unset makes the path ready (every write computes it from then on, so none is left unset after it)
841
+ for (;;) {
842
+ const swept = await this.host.onWriter(async () => {
843
+ if (this.stopped || this.host.rebuilding(p.source) || p.state !== "backfilling")
844
+ return "paused";
845
+ if (await this.fillChunk(p, `rowid IN (SELECT rowid FROM ${target} WHERE ${ident(p.name)} IS NULL LIMIT ${this.backfillRows}) AND `) > 0)
846
+ return "more";
847
+ p.marked = await this.markedRows(p);
848
+ Object.assign(p, { state: "ready", readyAt: new Date().toISOString(), filled: rows > 0 && ms > 0 ? { rows, seconds: ms / 1000 } : p.filled });
849
+ await this.record(this.store.writer, p);
850
+ this.cursors.delete(p.name);
851
+ this.measured.delete(measuredKey(p));
852
+ this.gen++;
853
+ return "ready";
854
+ });
855
+ if (swept !== "more")
856
+ break;
857
+ }
716
858
  if (p.state === "ready") {
717
859
  const took = rows > 0 && ms > 0 ? `, ${rows.toLocaleString("en-US")} by this fill in ${spanText(ms / 1000)}${p.fill === null ? "" : `, estimated at ${spanText(p.fill.seconds)} (${p.fill.basis === "measured" ? "from the last fill" : "modelled"})`}` : "";
718
860
  this.host.log(`${p.source}: ${pathText(p)} is stored: statements read it from its column (${p.rowsDone.toLocaleString("en-US")} row(s) filled${took})` +
719
- (p.marked > 0 ? `; ${p.marked.toLocaleString("en-US")} row(s) its function could not read are computed from their document when read (a Mason bug: report it, with mason logs)` : ""));
861
+ (p.marked > 0 ? `; ${await this.markedText(p, p.marked)}` : ""));
720
862
  }
721
863
  }
864
+ /**
865
+ * WHAT A PATH'S MARKED ROWS ARE, in words (the fill's end, the detector): a jsonb's key under a libc collation is
866
+ * marked where a string holds a character the measured order does not cover — the gap its sort is refused by
867
+ * (`libc-uncovered`), told apart by reading the marked rows' documents (`libcUncovered`, at most `MARKS_READ` of
868
+ * them); any other mark is a row its function could not read, a Mason bug.
869
+ */
870
+ async markedText(p, marked) {
871
+ let uncovered = 0, read = marked;
872
+ const libc = this.order?.libc;
873
+ if (isOrderRead(p) && this.current(p) && libc !== undefined) {
874
+ const docs = await this.host.onWriter(async () => (await this.store.writer.runAndReadAll(`SELECT ${ident(p.column)}::VARCHAR FROM ${tableOf(p.source)} ` +
875
+ `WHERE ${ident(p.name)} = ${recomputeSql(p)} LIMIT ${MARKS_READ}`)).getRowsJS());
876
+ read = docs.length;
877
+ uncovered = docs.filter(([d]) => d !== null && libcUncovered(libc, d)).length;
878
+ }
879
+ const of = read < marked ? ` (of the first ${read.toLocaleString("en-US")} read, of ${marked.toLocaleString("en-US")})` : "";
880
+ const said = [];
881
+ if (uncovered > 0)
882
+ said.push(`${uncovered.toLocaleString("en-US")} row(s) hold a character the database's libc order is not measured for, so a sort of them is refused by name (Mason gap libc-uncovered)`);
883
+ if (read > uncovered)
884
+ said.push(`${(read - uncovered).toLocaleString("en-US")} row(s) its function could not read are computed from their document when read (a Mason bug: report it, with mason logs)`);
885
+ return said.join("; ") + of;
886
+ }
722
887
  /** a column's rows marked to be computed at read time (KEPT_RECOMPUTE), on the caller's turn */
723
888
  async markedRows(p) {
724
- return Number((await this.store.writer.runAndReadAll(`SELECT count(*) FROM ${tableOf(p.source)} WHERE ${ident(p.name)} = ${RECOMPUTE_SQL}`)).getRowsJS()[0][0]);
889
+ return Number((await this.store.writer.runAndReadAll(`SELECT count(*) FROM ${tableOf(p.source)} WHERE ${ident(p.name)} = ${recomputeSql(p)}`)).getRowsJS()[0][0]);
725
890
  }
726
891
  /** a READY path's marked rows counted again (its table swapped in, the detector): a change of exactness moves the
727
892
  * plans' generation, so a plan that read the column alone is made again as the CASE, and the other way */
@@ -742,7 +907,7 @@ export class KeptJsonPaths {
742
907
  const w = this.store.writer;
743
908
  await w.run("BEGIN TRANSACTION");
744
909
  try {
745
- const n = Number((await w.run(`UPDATE ${tableOf(p.source)} SET ${ident(p.name)} = ${keptValueSql(p, ident(p.column))} WHERE ${where}${ident(p.name)} IS NULL`)).rowsChanged);
910
+ const n = Number((await w.run(`UPDATE ${tableOf(p.source)} SET ${ident(p.name)} = ${this.valueSql(p, ident(p.column))} WHERE ${where}${ident(p.name)} IS NULL`)).rowsChanged);
746
911
  await w.run(`UPDATE ${PATHS_TABLE} SET rows_done = rows_done + ${n} WHERE name = ${literal(p.name)}`);
747
912
  await w.run("COMMIT");
748
913
  p.rowsDone += n;
@@ -756,9 +921,9 @@ export class KeptJsonPaths {
756
921
  /** THE DETECTOR: a READY column's rows still unset (a write that did not compute it: a Mason bug), filled and said;
757
922
  * and its marked rows counted again (`settle`) */
758
923
  async check(p) {
759
- await this.host.onWriter(async () => {
924
+ const marked = await this.host.onWriter(async () => {
760
925
  if (this.stopped || this.host.rebuilding(p.source) || p.state !== "ready")
761
- return;
926
+ return 0;
762
927
  const unset = Number((await this.store.writer.runAndReadAll(`SELECT count(*) FROM ${tableOf(p.source)} WHERE ${ident(p.name)} IS NULL`)).getRowsJS()[0][0]);
763
928
  if (unset > 0) {
764
929
  this.host.log(`${p.source}: ${unset} row(s) were written without the stored value of ${pathText(p)} (a Mason bug: report it, with mason logs); they are filled now`);
@@ -767,14 +932,16 @@ export class KeptJsonPaths {
767
932
  }
768
933
  const was = p.marked;
769
934
  await this.settle(p);
770
- if (p.marked > was)
771
- this.host.log(`${p.source}: ${p.marked} row(s) of ${pathText(p)} could not be read by its function (a Mason bug: report it, with mason logs); its reads compute them from their document`);
935
+ return p.marked > was ? p.marked : 0;
772
936
  });
937
+ // (the words read the table again on a writer's turn of their own, never inside the detector's)
938
+ if (marked > 0)
939
+ this.host.log(`${p.source}: ${pathText(p)} has marked rows, read through their document from now on: ${await this.markedText(p, marked)}`);
773
940
  }
774
941
  /** THE ORACLE'S (`eager`): every read's path stored and filled now, whatever the request log says */
775
942
  async keepAll(reads) {
776
943
  for (const r of reads) {
777
- const read = pathOf(r);
944
+ const read = this.own(pathOf(r));
778
945
  const name = hiddenName(read);
779
946
  if (this.paths.get(name)?.state === "ready" || !this.storable(read))
780
947
  continue;
@@ -233,6 +233,9 @@ export function typesByLocation(tree) {
233
233
  const relation = v._node === "VAR" ? relationOf(v, rtables) : null;
234
234
  if (relation !== null)
235
235
  typed.relation = relation;
236
+ const fromFunction = v._node === "VAR" ? functionColumnOf(v, rtables) : null;
237
+ if (fromFunction !== null)
238
+ typed.functionColumn = fromFunction;
236
239
  const list = out.get(loc);
237
240
  if (list)
238
241
  list.push(typed);
@@ -255,6 +258,17 @@ function relationOf(v, rtables) {
255
258
  const relid = rte === undefined ? null : num(rte.relid ?? null);
256
259
  return rte === undefined || attno === null || attno < 1 || num(rte.rtekind ?? null) !== RTE_RELATION || relid === null || relid === 0 ? null : { relid, attno };
257
260
  }
261
+ const RTE_FUNCTION = 3;
262
+ /** Typed.functionColumn */
263
+ function functionColumnOf(v, rtables) {
264
+ const up = num(v.varlevelsup ?? null) ?? 0, varno = num(v.varno ?? null), attno = num(v.varattno ?? null);
265
+ const rte = varno === null ? undefined : rtables[rtables.length - 1 - up]?.[varno - 1];
266
+ if (rte === undefined || attno === null || attno < 1 || num(rte.rtekind ?? null) !== RTE_FUNCTION || !isNode(rte.eref ?? null))
267
+ return null;
268
+ const eref = rte.eref;
269
+ const column = strings(eref.colnames)[attno - 1];
270
+ return typeof eref.aliasname === "string" && column !== undefined ? { alias: eref.aliasname, column } : null;
271
+ }
258
272
  const strings = (v) => Array.isArray(v) ? v.map((x) => typeof x === "string" ? x : "") : [];
259
273
  /** Typed.qualifier: the name of a VAR's FROM item when another FROM item of its query has the column in another case,
260
274
  * or when the VAR is an outer query's (a correlated subquery's bare `i4`, which DuckDB binds to the subquery's own
@@ -327,6 +341,32 @@ export function caseTwins(tree) {
327
341
  visit(tree);
328
342
  return out;
329
343
  }
344
+ /**
345
+ * Whether a JOIN … USING or a NATURAL JOIN of the analysed tree joins on a column of one of `types` (jsonb, jsonb[]):
346
+ * Postgres equates the columns by that type's `=`, which DuckDB's USING would not (it compares the text). The join's
347
+ * condition is Postgres's own (its `quals`), at no offset of the statement, so it is read here, not by the rewrite.
348
+ */
349
+ export function joinsUsing(tree, types) {
350
+ const compares = (v) => {
351
+ if (Array.isArray(v))
352
+ return v.some(compares);
353
+ if (v === undefined || !isNode(v))
354
+ return false;
355
+ if (v._node === "OPEXPR" && Array.isArray(v.args) && v.args.some((a) => isNode(a) && types.has(exprType(a)?.type ?? 0)))
356
+ return true;
357
+ return Object.entries(v).some(([k, x]) => k !== "_node" && compares(x));
358
+ };
359
+ const visit = (v) => {
360
+ if (Array.isArray(v))
361
+ return v.some(visit);
362
+ if (v === undefined || !isNode(v))
363
+ return false;
364
+ if (v._node === "JOINEXPR" && Array.isArray(v.usingClause) && v.usingClause.length > 0 && compares(v.quals))
365
+ return true;
366
+ return Object.entries(v).some(([k, x]) => k !== "_node" && visit(x));
367
+ };
368
+ return visit(tree);
369
+ }
330
370
  /** The analysed query's output columns, in order (resjunk ones, which a sort may add, left out): their types. */
331
371
  export function outputTypes(tree) {
332
372
  if (!isNode(tree) || tree._node !== "QUERY" || !Array.isArray(tree.targetList))