@mason-data/cli 0.8.0 → 0.10.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 (118) hide show
  1. package/README.md +19 -8
  2. package/dist/src/api/error.js +4 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +166 -32
  5. package/dist/src/api/postgres/listener.js +11 -3
  6. package/dist/src/api/postgres/proxy.js +129 -0
  7. package/dist/src/api/postgres/schema.js +6 -0
  8. package/dist/src/api/postgres/server.js +527 -103
  9. package/dist/src/api/postgres/translate.js +47 -2
  10. package/dist/src/cli/background.js +18 -19
  11. package/dist/src/cli/bridge-stdin.js +101 -0
  12. package/dist/src/cli/command.js +337 -15
  13. package/dist/src/cli/commands.js +458 -169
  14. package/dist/src/cli/credentials.js +182 -0
  15. package/dist/src/cli/demo.js +2 -2
  16. package/dist/src/cli/deploy.js +680 -0
  17. package/dist/src/cli/dev.js +148 -27
  18. package/dist/src/cli/doctor.js +122 -36
  19. package/dist/src/cli/generated-files.js +143 -0
  20. package/dist/src/cli/guides.js +116 -16
  21. package/dist/src/cli/hosted.js +850 -0
  22. package/dist/src/cli/init.js +154 -33
  23. package/dist/src/cli/login.js +292 -0
  24. package/dist/src/cli/machine.js +125 -40
  25. package/dist/src/cli/main.js +59 -10
  26. package/dist/src/cli/mason.cjs +17 -0
  27. package/dist/src/cli/mcp.js +17 -14
  28. package/dist/src/cli/output-gate.js +176 -0
  29. package/dist/src/cli/page-share.js +96 -0
  30. package/dist/src/cli/pg-tls.js +1 -1
  31. package/dist/src/cli/pin.js +6 -2
  32. package/dist/src/cli/pool-size.cjs +85 -0
  33. package/dist/src/cli/ports.js +2 -0
  34. package/dist/src/cli/preflight.js +130 -39
  35. package/dist/src/cli/project.js +100 -1
  36. package/dist/src/cli/readiness.js +42 -6
  37. package/dist/src/cli/registry.js +89 -36
  38. package/dist/src/cli/replica-hosted.js +1324 -0
  39. package/dist/src/cli/replica.js +114 -0
  40. package/dist/src/cli/replication-mode.js +5 -0
  41. package/dist/src/cli/request-filter.js +16 -6
  42. package/dist/src/cli/roles.js +2 -1
  43. package/dist/src/cli/run-server.js +179 -9
  44. package/dist/src/cli/secret-input.js +193 -0
  45. package/dist/src/cli/security.js +57 -10
  46. package/dist/src/cli/serve.js +65 -9
  47. package/dist/src/cli/share-run.js +51 -8
  48. package/dist/src/cli/share.js +23 -18
  49. package/dist/src/cli/source-edit.js +10 -4
  50. package/dist/src/cli/source-role.js +343 -0
  51. package/dist/src/cli/sources.js +50 -23
  52. package/dist/src/cli/style.js +16 -1
  53. package/dist/src/cli/supervise.js +15 -23
  54. package/dist/src/cli/sync.js +17 -8
  55. package/dist/src/cli/token-hosted.js +343 -0
  56. package/dist/src/cli/tunnel.js +1 -1
  57. package/dist/src/cli/ui-hosted.js +163 -0
  58. package/dist/src/cli/ui-ssh.js +460 -0
  59. package/dist/src/cli/ui.js +162 -17
  60. package/dist/src/cli/usage.js +19 -15
  61. package/dist/src/cli/user-hosted.js +364 -0
  62. package/dist/src/cli/user.js +275 -105
  63. package/dist/src/demo/seed.js +26 -0
  64. package/dist/src/duck/analyze.js +36 -20
  65. package/dist/src/duck/collation.js +266 -0
  66. package/dist/src/duck/constant-key.js +94 -0
  67. package/dist/src/duck/distinct-key.js +283 -26
  68. package/dist/src/duck/events.js +39 -8
  69. package/dist/src/duck/executor-worker.js +2 -2
  70. package/dist/src/duck/executor.js +6 -5
  71. package/dist/src/duck/explain.js +4 -0
  72. package/dist/src/duck/functions.js +448 -49
  73. package/dist/src/duck/gaps.js +34 -16
  74. package/dist/src/duck/generated-plan.js +1070 -0
  75. package/dist/src/duck/generated.js +906 -0
  76. package/dist/src/duck/grants.js +1 -1
  77. package/dist/src/duck/held-types.js +64 -0
  78. package/dist/src/duck/history.js +4 -0
  79. package/dist/src/duck/jsonb-order.js +9 -12
  80. package/dist/src/duck/kept-route.js +6 -5
  81. package/dist/src/duck/kept.js +9 -7
  82. package/dist/src/duck/late.js +1469 -40
  83. package/dist/src/duck/libc-bound.js +341 -0
  84. package/dist/src/duck/libc-order.js +892 -126
  85. package/dist/src/duck/pg-eval-worker.js +7 -1
  86. package/dist/src/duck/pg-eval.js +6 -4
  87. package/dist/src/duck/pgtree.js +774 -23
  88. package/dist/src/duck/plan.js +275 -30
  89. package/dist/src/duck/publication.js +14 -0
  90. package/dist/src/duck/requests.js +9 -5
  91. package/dist/src/duck/resolved.js +117 -2
  92. package/dist/src/duck/rewrite.js +4567 -583
  93. package/dist/src/duck/row-filter.js +12 -6
  94. package/dist/src/duck/statement.js +19 -0
  95. package/dist/src/duck/store.js +294 -68
  96. package/dist/src/duck/stream.js +9 -5
  97. package/dist/src/duck/sync.js +602 -126
  98. package/dist/src/duck/token.js +4 -1
  99. package/dist/src/duck/types.js +63 -7
  100. package/dist/src/duck/usage.js +6 -4
  101. package/dist/src/duck/users.js +110 -9
  102. package/dist/src/duck/verified.js +159 -2
  103. package/dist/src/duck/views.js +4 -3
  104. package/dist/src/lib/cidr.js +177 -0
  105. package/dist/src/lib/database-url.js +55 -10
  106. package/dist/src/lib/instant.js +50 -0
  107. package/dist/src/lib/log-buffer.js +13 -4
  108. package/dist/src/lib/log-read.js +196 -0
  109. package/dist/src/lib/pg-numeric.js +387 -49
  110. package/dist/src/lib/secret-mask.js +142 -0
  111. package/dist/src/lib/table-sync.js +84 -19
  112. package/dist/src/web/app.js +9 -4
  113. package/dist/src/web/pages/status.js +178 -13
  114. package/dist/src/web/pages/syncs.js +1 -1
  115. package/dist/src/web/pages/tables.js +8 -3
  116. package/dist/src/web/pages/users.js +4 -3
  117. package/dist/src/web/shared.js +19 -3
  118. package/package.json +8 -7
@@ -1,6 +1,9 @@
1
1
  /** pg_proc's count(any) */
2
2
  const COUNT_ANY = 2147;
3
3
  const RTE_RELATION = 0;
4
+ const RTE_SUBQUERY = 1;
5
+ const RTE_CTE = 6;
6
+ const RTE_GROUP = 9;
4
7
  /** JoinType: INNER, LEFT, FULL, RIGHT (nodes.h); the planner's SEMI / ANTI never reach a parse tree */
5
8
  const JOINS = new Set([0, 1, 2, 3]);
6
9
  export const columnKey = (relid, attno) => `${relid}.${attno}`;
@@ -39,22 +42,25 @@ export function distinctCountTables(tree) {
39
42
  return counts ? [...relids].sort((a, b) => a - b) : [];
40
43
  }
41
44
  /**
42
- * The catalog Postgres's single-column PRIMARY KEYS of `relids` (never another unique index: the module's doc), with
43
- * each one's collation and the equality operators of its btree family. A primary key is valid, unique and over a plain
44
- * column; the conditions are written out all the same, so the rule is this query's and not a reader's memory.
45
+ * The catalog Postgres's PRIMARY KEYS of `relids` (never another unique index: the module's doc), a row per column of
46
+ * the key, with its collation, the equality operators of its btree family and how many columns the key has. A primary
47
+ * key is valid, unique and over plain columns; the conditions are written out all the same, so the rule is this
48
+ * query's and not a reader's memory.
45
49
  */
46
50
  export function uniqueColumnsSql(relids) {
47
- return "SELECT i.indrelid::text, i.indkey[0]::text, i.indcollation[0]::text, coalesce(string_agg(o.amopopr::text, ' ' ORDER BY o.amopopr), '') " +
48
- "FROM pg_index i JOIN pg_opclass c ON c.oid = i.indclass[0] JOIN pg_am m ON m.oid = c.opcmethod AND m.amname = 'btree' " +
51
+ return "SELECT i.indrelid::text, i.indkey[k.at]::text, i.indcollation[k.at]::text, coalesce(string_agg(o.amopopr::text, ' ' ORDER BY o.amopopr), ''), i.indnkeyatts::text " +
52
+ "FROM pg_index i CROSS JOIN LATERAL generate_series(0, i.indnkeyatts - 1) AS k(at) JOIN pg_opclass c ON c.oid = i.indclass[k.at] JOIN pg_am m ON m.oid = c.opcmethod AND m.amname = 'btree' " +
49
53
  "LEFT JOIN pg_amop o ON o.amopfamily = c.opcfamily AND o.amopmethod = c.opcmethod AND o.amopstrategy = 3 " +
50
- `WHERE i.indrelid IN (${relids.map((r) => String(Math.trunc(r))).join(", ")}) AND i.indisprimary AND i.indisunique AND i.indisvalid AND i.indnatts = 1 AND i.indnkeyatts = 1 ` +
51
- "AND i.indkey[0] > 0 AND i.indpred IS NULL AND i.indexprs IS NULL GROUP BY 1, 2, 3";
54
+ `WHERE i.indrelid IN (${relids.map((r) => String(Math.trunc(r))).join(", ")}) AND i.indisprimary AND i.indisunique AND i.indisvalid AND i.indnatts = i.indnkeyatts ` +
55
+ "AND i.indkey[k.at] > 0 AND i.indpred IS NULL AND i.indexprs IS NULL GROUP BY 1, 2, 3, 5";
52
56
  }
53
- /** `uniqueColumnsSql`'s rows, as the decision reads them (a table has one primary key; the merge is for a second row
54
- * of one column, which no catalog writes) */
57
+ /** `uniqueColumnsSql`'s rows of a key of one column, as the decision reads them (a table has one primary key; the merge
58
+ * is for a second row of one column, which no catalog writes) */
55
59
  export function uniqueColumnsOf(rows) {
56
60
  const out = new Map();
57
- for (const [relid, attno, collation, ops] of rows) {
61
+ for (const [relid, attno, collation, ops, columns] of rows) {
62
+ if (columns !== "1")
63
+ continue;
58
64
  const key = columnKey(Number(relid), Number(attno));
59
65
  const equalities = new Set((ops ?? "").split(" ").filter((x) => x !== "").map(Number));
60
66
  const had = out.get(key);
@@ -67,6 +73,17 @@ export function uniqueColumnsOf(rows) {
67
73
  }
68
74
  return out;
69
75
  }
76
+ /** `uniqueColumnsSql`'s rows, each table's whole key: a key not read whole is no key */
77
+ export function primaryKeysOf(rows) {
78
+ const out = new Map(), whole = new Map();
79
+ for (const [relid, attno, collation, ops, columns] of rows) {
80
+ const key = out.get(Number(relid)) ?? new Map();
81
+ key.set(Number(attno), { collation: Number(collation ?? 0), equalities: new Set((ops ?? "").split(" ").filter((x) => x !== "").map(Number)) });
82
+ out.set(Number(relid), key);
83
+ whole.set(Number(relid), Number(columns));
84
+ }
85
+ return new Map([...out].filter(([relid, key]) => key.size === whole.get(relid)));
86
+ }
70
87
  /** a VAR of this query's own FROM (varlevelsup 0), bare or under a binary-compatible relabel (a varchar read as text) */
71
88
  function ownVar(v) {
72
89
  if (!isNode(v))
@@ -118,28 +135,33 @@ function baseTable(rtable, ref) {
118
135
  const relid = num(rte.relid);
119
136
  return rtindex !== null && relid !== null && relid > 0 ? { rtindex, relid } : null;
120
137
  }
138
+ /**
139
+ * The column of the table at `rtindex` a conjunct equates with a column of `side`, both bare, by the operator and under
140
+ * the collation `key` says are that column's own; null where it does not.
141
+ */
142
+ function equated(c, side, rtindex, key) {
143
+ if (c._node !== "OPEXPR" || list(c.args).length !== 2)
144
+ return null;
145
+ const [a, b] = list(c.args).map(ownVar);
146
+ if (a == null || b == null)
147
+ return null;
148
+ for (const [mine, theirs] of [[a, b], [b, a]]) {
149
+ if (num(theirs.varno) !== rtindex || !side.has(num(mine.varno) ?? -1))
150
+ continue;
151
+ const attno = num(theirs.varattno), u = key(attno);
152
+ const collation = num(c.inputcollid) ?? 0;
153
+ if (u !== undefined && u.equalities.has(num(c.opno) ?? -1) && (collation === 0 || collation === u.collation))
154
+ return attno;
155
+ }
156
+ return null;
157
+ }
121
158
  /**
122
159
  * Whether a join's condition matches each row of its `side` with at most ONE row of `other`, a base table: a conjunct
123
160
  * that is an equality, by the operator of `other`'s single-column primary key and under its collation, of that
124
161
  * column (bare) with a column of `side` (bare).
125
162
  */
126
163
  function atMostOne(quals, side, other, unique) {
127
- return conjuncts(quals).some((c) => {
128
- if (c._node !== "OPEXPR" || list(c.args).length !== 2)
129
- return false;
130
- const [a, b] = list(c.args).map(ownVar);
131
- if (a == null || b == null)
132
- return false;
133
- for (const [mine, theirs] of [[a, b], [b, a]]) {
134
- if (num(theirs.varno) !== other.rtindex || !side.has(num(mine.varno) ?? -1))
135
- continue;
136
- const u = unique.get(columnKey(other.relid, num(theirs.varattno)));
137
- const collation = num(c.inputcollid) ?? 0;
138
- if (u !== undefined && u.equalities.has(num(c.opno) ?? -1) && (collation === 0 || collation === u.collation))
139
- return true;
140
- }
141
- return false;
142
- });
164
+ return conjuncts(quals).some((c) => equated(c, side, other.rtindex, (attno) => unique.get(columnKey(other.relid, attno))) !== null);
143
165
  }
144
166
  /**
145
167
  * Whether the FROM of `query` holds each row of the table at `rtindex` at most once: its one item, that table or a
@@ -169,6 +191,241 @@ export function notMultiplied(query, rtindex, unique) {
169
191
  };
170
192
  return down(from[0]);
171
193
  }
194
+ /** whether a query is a projection and a WHERE alone: no aggregate, window, set-returning list, subquery expression,
195
+ * DISTINCT, grouping, sort, limit, set operation, WITH or row lock, so each of its rows is one row of its FROM */
196
+ export function projectionOnly(q) {
197
+ for (const f of ["hasAggs", "hasWindowFuncs", "hasTargetSRFs", "hasSubLinks", "hasDistinctOn", "hasRecursive", "hasModifyingCTE", "hasForUpdate"])
198
+ if (q[f] !== "false")
199
+ return false;
200
+ for (const f of ["cteList", "groupClause", "groupingSets", "havingQual", "windowClause", "distinctClause", "sortClause", "limitOffset", "limitCount", "rowMarks", "setOperations"]) {
201
+ if (q[f] !== null && q[f] !== undefined && !(Array.isArray(q[f]) && q[f].length === 0))
202
+ return false;
203
+ }
204
+ return true;
205
+ }
206
+ /**
207
+ * THE PLAIN READ: a query that is a projection and a WHERE over ONE table, or over one plain read (a view of a view), so
208
+ * it holds each row of that table at most once; null for anything else. Whether it may also be read twice is the
209
+ * reader's (src/duck/late.ts asks too that nothing in it is volatile): this is about its rows alone.
210
+ */
211
+ export function plainTable(q) {
212
+ if (!projectionOnly(q) || !isNode(q.jointree))
213
+ return null;
214
+ const from = list(q.jointree.fromlist);
215
+ const rtable = list(q.rtable);
216
+ if (from.length !== 1)
217
+ return null;
218
+ const inner = heldTable(q, rtable, from[0]);
219
+ if (inner === null)
220
+ return null;
221
+ const outputs = list(q.targetList).filter((t) => isNode(t) && t.resjunk !== "true");
222
+ return { relid: inner.relid, column: (attno) => {
223
+ const v = ownVar(outputs[attno - 1]?.expr);
224
+ return v === null ? null : inner.column(num(v.varattno));
225
+ } };
226
+ }
227
+ /** the table a FROM's RANGETBLREF reads once: a base table, a plain derived read, or a plain read of its query's WITH */
228
+ function heldTable(query, rtable, ref) {
229
+ const t = baseTable(rtable, ref);
230
+ if (t !== null)
231
+ return { relid: t.relid, column: (attno) => attno };
232
+ if (!isNode(ref) || ref._node !== "RANGETBLREF")
233
+ return null;
234
+ const rte = rtable[(num(ref.rtindex) ?? 0) - 1];
235
+ // a LATERAL read is one per row of what it follows: its rows are no bound by its table's
236
+ if (!isNode(rte) || rte.lateral === "true")
237
+ return null;
238
+ if (num(rte.rtekind) === RTE_SUBQUERY && isNode(rte.subquery))
239
+ return plainTable(rte.subquery);
240
+ if (num(rte.rtekind) !== RTE_CTE || (num(rte.ctelevelsup) ?? -1) !== 0)
241
+ return null;
242
+ const cte = list(query.cteList).find((c) => isNode(c) && c.ctename === rte.ctename);
243
+ return isNode(cte) && isNode(cte.ctequery) ? plainTable(cte.ctequery) : null;
244
+ }
245
+ /**
246
+ * THE FAN-OUT PROOF: whether every join of `query` equates a whole primary key of a table whose rows its side holds at
247
+ * most once, so the FROM's rows are bounded by the sum of its tables'. A key must be unique IN ITS SIDE, not merely in
248
+ * its table: `a JOIN b ON a.id = b.a_id JOIN c ON a.id = c.a_id` equates a's key twice, and an `a` of a thousand b's and
249
+ * a thousand c's is a million rows, because the first join repeats `a`. So a join passes on, as held at most once, only
250
+ * the tables of a side whose every row it matches at most once.
251
+ *
252
+ * Each key column in an equality, by its btree family's operator and under its collation, with a bare column of the
253
+ * other side (`equated`); conjuncts of the join's top-level AND only, so an OR proves nothing and any other conjunct
254
+ * only filters. INNER, LEFT, RIGHT and FULL alike: a side's unmatched row is one row, and its NULL-extended key
255
+ * equals nothing. A comma's items are inner joins, left to right, on the WHERE's conjuncts. Everything else (a cross
256
+ * join, a key in part, a partial or UNIQUE index, no key, a key equated with an expression or a constant, a FROM item
257
+ * neither a base table nor a plain read of one, a JOIN's condition written in the WHERE) proves nothing.
258
+ *
259
+ * What it decides is what a LIMIT with no ORDER BY costs and whether its pages are stable, never the rows' values, so
260
+ * a key stale in the catalog (the source's as of the start) costs time, not an answer.
261
+ */
262
+ export function fanOutBounded(query, keys) {
263
+ const jt = query.jointree;
264
+ if (!isNode(jt))
265
+ return false;
266
+ const rtable = list(query.rtable);
267
+ const joined = (left, right, quals, indexes) => {
268
+ if (left === null || right === null)
269
+ return null;
270
+ const matchedOnce = (s, by) => [...s.once].some(([rtindex, table]) => {
271
+ const key = keys.get(table.relid);
272
+ if (key === undefined)
273
+ return false;
274
+ const keyColumn = (attno) => { const c = table.column(attno); return c === null ? undefined : key.get(c); };
275
+ const met = new Set(quals.map((c) => { const at = equated(c, by.indexes, rtindex, keyColumn); return at === null ? null : table.column(at); }));
276
+ return [...key.keys()].every((attno) => met.has(attno));
277
+ });
278
+ const leftMatchesOne = matchedOnce(right, left), rightMatchesOne = matchedOnce(left, right);
279
+ if (!leftMatchesOne && !rightMatchesOne)
280
+ return null;
281
+ return { indexes, once: new Map([...(leftMatchesOne ? left.once : []), ...(rightMatchesOne ? right.once : [])]) };
282
+ };
283
+ const side = (n) => {
284
+ if (isNode(n) && n._node === "JOINEXPR" && JOINS.has(num(n.jointype) ?? -1) && isNode(n.larg) && isNode(n.rarg))
285
+ return joined(side(n.larg), side(n.rarg), conjuncts(n.quals), indexesOf(n));
286
+ const t = heldTable(query, rtable, n);
287
+ const rtindex = isNode(n) ? num(n.rtindex) : null;
288
+ return t === null || rtindex === null ? null : { indexes: new Set([rtindex]), once: new Map([[rtindex, t]]) };
289
+ };
290
+ const where = conjuncts(jt.quals);
291
+ const items = list(jt.fromlist).map(side);
292
+ if (items.length === 0)
293
+ return false;
294
+ return items.reduce((l, r) => joined(l, r, where, new Set([...l?.indexes ?? [], ...r?.indexes ?? []]))) !== null;
295
+ }
296
+ /** the base tables a query reads, at any depth (a wrapper's LIMIT stands over the join inside it) */
297
+ function tablesUnder(q) {
298
+ const out = [];
299
+ const visit = (v) => {
300
+ if (Array.isArray(v)) {
301
+ v.forEach(visit);
302
+ return;
303
+ }
304
+ if (!isNode(v))
305
+ return;
306
+ if (v._node === "RANGETBLENTRY" && num(v.rtekind) === RTE_RELATION) {
307
+ const r = num(v.relid) ?? 0;
308
+ if (r > 0)
309
+ out.push(r);
310
+ }
311
+ for (const [k, x] of Object.entries(v))
312
+ if (k !== "_node")
313
+ visit(x);
314
+ };
315
+ visit(q);
316
+ return out;
317
+ }
318
+ /** The base tables `fanOutBounded` may be asked about: those under a query with a LIMIT and no sort, at least two of them
319
+ * (none: the plan asks the catalog nothing). */
320
+ export function fanOutTables(tree) {
321
+ const relids = new Set();
322
+ const visit = (v) => {
323
+ if (Array.isArray(v)) {
324
+ v.forEach(visit);
325
+ return;
326
+ }
327
+ if (!isNode(v))
328
+ return;
329
+ if (v._node === "QUERY" && isNode(v.limitCount) && list(v.sortClause).length === 0) {
330
+ const tables = tablesUnder(v);
331
+ if (tables.length >= 2)
332
+ for (const r of tables)
333
+ relids.add(r);
334
+ }
335
+ for (const [k, x] of Object.entries(v))
336
+ if (k !== "_node")
337
+ visit(x);
338
+ };
339
+ visit(tree);
340
+ return [...relids].sort((a, b) => a - b);
341
+ }
342
+ /** a grouped expression's collation where it is a bare column (a VAR's, or a binary-compatible relabel's own) */
343
+ const collationOf = (v) => !isNode(v) ? 0 : v._node === "RELABELTYPE" ? num(v.resultcollid) ?? 0 : num(v.varcollid) ?? 0;
344
+ /**
345
+ * THE KEY-FIRST GROUPING PROOF (issue 134, step 6; src/duck/late.ts `keyFirst`): the base tables of `query` whose WHOLE
346
+ * primary key it groups by, by rtindex, each with its key's columns by attno. A group then holds one row of that table
347
+ * (or none, where an outer join supplied none), so every column of it is decided by the group, and the row can be read
348
+ * by its row number after the limit. Each key column grouped bare (a binary-compatible relabel allowed), by its btree
349
+ * family's equality and under its collation; a grouping set proves nothing (a key reads NULL in its total rows). The
350
+ * key is the primary key, never a UNIQUE index (the module's doc): the copy is written by it. A key the copy holds
351
+ * twice for a moment (a change applied in pieces) is grouped as written, never by a row number, so no group is split.
352
+ * A SELECT DISTINCT that is a grouping on its list (`distinctGrouping`) is read as its GROUP BY: its distinctClause.
353
+ */
354
+ export function keyGrouped(query, keys) {
355
+ const out = new Map();
356
+ if (list(query.groupingSets).length > 0)
357
+ return out;
358
+ const clause = list(query.groupClause).length > 0 ? list(query.groupClause) : distinctGrouping(query) ? list(query.distinctClause) : [];
359
+ const targets = list(query.targetList).filter(isNode);
360
+ const rtable = list(query.rtable);
361
+ const grouped = new Map();
362
+ // a grouped output is a VAR of the query's RTE_GROUP entry (Postgres 17+): the expression grouped is its groupexprs'
363
+ const grouped1 = (e) => {
364
+ const v = ownVar(e), rte = v === null ? undefined : rtable[(num(v.varno) ?? 0) - 1];
365
+ return isNode(rte) && num(rte.rtekind) === RTE_GROUP ? list(rte.groupexprs)[(num(v.varattno) ?? 0) - 1] : e;
366
+ };
367
+ for (const g of clause) {
368
+ if (!isNode(g))
369
+ continue;
370
+ const t = targets.find((x) => x.ressortgroupref === g.tleSortGroupRef);
371
+ const expr = grouped1(t?.expr);
372
+ const v = ownVar(expr);
373
+ const rtindex = num(v?.varno), attno = num(v?.varattno);
374
+ if (v === null || rtindex === null || attno === null)
375
+ continue;
376
+ const table = baseTable(rtable, { _node: "RANGETBLREF", rtindex: String(rtindex) });
377
+ const key = table === null ? undefined : keys.get(table.relid)?.get(attno);
378
+ const collation = collationOf(expr);
379
+ if (key === undefined || !key.equalities.has(num(g.eqop) ?? -1) || (collation !== 0 && collation !== key.collation))
380
+ continue;
381
+ grouped.set(rtindex, new Set([...grouped.get(rtindex) ?? [], attno]));
382
+ }
383
+ for (const [rtindex, attnos] of grouped) {
384
+ const table = baseTable(rtable, { _node: "RANGETBLREF", rtindex: String(rtindex) });
385
+ const key = [...keys.get(table.relid).keys()];
386
+ if (key.every((a) => attnos.has(a)))
387
+ out.set(rtindex, key);
388
+ }
389
+ return out;
390
+ }
391
+ /**
392
+ * Whether a query is a SELECT DISTINCT that is a grouping on its list (issue 134, step 6, task D): a DISTINCT, not a
393
+ * DISTINCT ON (which keeps one row of each set, a different thing), with no GROUP BY beside it, and nothing in its list
394
+ * computed before the DISTINCT that a grouping would compute after it: no aggregate (a DISTINCT over one row), window
395
+ * function, set-returning function or subquery. Such a DISTINCT's rows are its GROUP BY's, its list's equalities the
396
+ * same, so `keyGrouped` reads its distinctClause as it reads a groupClause.
397
+ */
398
+ export function distinctGrouping(q) {
399
+ if (list(q.distinctClause).length === 0 || list(q.groupClause).length > 0 || q.hasDistinctOn !== "false")
400
+ return false;
401
+ return ["hasAggs", "hasWindowFuncs", "hasTargetSRFs", "hasSubLinks"].every((f) => q[f] === "false");
402
+ }
403
+ /** The base tables `keyGrouped` may be asked about: those of a query that groups, or is a DISTINCT, and has a LIMIT
404
+ * (none: the plan asks the catalog nothing) */
405
+ export function groupedLimitTables(tree) {
406
+ const relids = new Set();
407
+ const visit = (v) => {
408
+ if (Array.isArray(v)) {
409
+ v.forEach(visit);
410
+ return;
411
+ }
412
+ if (!isNode(v))
413
+ return;
414
+ if (v._node === "QUERY" && isNode(v.limitCount) && (list(v.groupClause).length > 0 || distinctGrouping(v))) {
415
+ for (const e of list(v.rtable))
416
+ if (isNode(e) && num(e.rtekind) === RTE_RELATION) {
417
+ const r = num(e.relid) ?? 0;
418
+ if (r > 0)
419
+ relids.add(r);
420
+ }
421
+ }
422
+ for (const [k, x] of Object.entries(v))
423
+ if (k !== "_node")
424
+ visit(x);
425
+ };
426
+ visit(tree);
427
+ return [...relids].sort((a, b) => a - b);
428
+ }
172
429
  /** Whether this AGGREF of `query` is a count(DISTINCT x) whose DISTINCT changes nothing (the module's doc). */
173
430
  function distinctOfKey(query, a, unique) {
174
431
  if (!isDistinctCount(a) || (num(a.agglevelsup) ?? 0) !== 0 || a.aggkind !== "n" || a.aggstar === "true" || list(a.aggorder).length > 0)
@@ -1,19 +1,46 @@
1
+ /**
2
+ * THE SYNC'S EVENTS (`_mason.sync_events`): a copy failed, a poll failed, a catch-up failed, the stream failed or
3
+ * refused a table, the database could not be reached — and the notable good news beside them: a copy finished, a
4
+ * table current again after a failure. Before this a sync failure was kept only as a table's CURRENT error (the next
5
+ * success wiped it) and, before that one, only in the log file: `mason status` could not say what went wrong an hour ago.
6
+ *
7
+ * Kept as the request log is kept (src/duck/history.ts, the frame both share): rows wait here for at most FLUSH_MS and go in ONE INSERT, on a
8
+ * connection of the events' own — never the sync's writer turn (`onWriter`), which a swap or a stream batch may hold
9
+ * for seconds; a read is on a third connection and sees committed batches only; a write that fails is said once with
10
+ * the events it cost, and again when a write succeeds after it, and it NEVER fails the sync that recorded it (the
11
+ * sync's own state and log line are written either way). Kept `MASON_HISTORY_DAYS` days.
12
+ *
13
+ * A failure repeated word for word is ONE event (`REPEAT_MS`): a database away fails every pass, every few seconds,
14
+ * and a list of the same line buries the one before it. It is written again once it was over in between, or an hour
15
+ * later: a table's failure is over when the table is current again (`recovered`) or copied anew (`copied`); a failure
16
+ * of the whole sync (`table: null`: the stream, the database) when the sync says it is (`recovered` of no table: the
17
+ * stream applied a batch again, the database attached again), which ends every whole-sync failure's repeat at once —
18
+ * an extra event at worst, never a failure dropped. While it lasts, the table's own `error` (mason status) says it.
19
+ *
20
+ * The run's RESTARTS are not here: the supervisor that records them is a process of its own, which cannot open the
21
+ * file DuckDB holds, so it writes `.mason/restarts.json` (src/cli/supervise.ts) and the reader merges them
22
+ * (`withRestarts`).
23
+ *
24
+ * The Postgres catalog never lists `_mason`, so a tool connected to the listener cannot read the events.
25
+ */
26
+ import { maskPasswordKeyword } from "../lib/secret-mask.js";
1
27
  import { MASON_SCHEMA } from "./store.js";
2
- import { Batches, History, Streak } from "./history.js";
28
+ import { Batches, History, Streak, cutoff } from "./history.js";
3
29
  export const EVENTS_TABLE = `${MASON_SCHEMA}.sync_events`;
4
30
  export const EVENTS_DDL = `CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (happened_at TIMESTAMPTZ, seq BIGINT, kind VARCHAR, table_name VARCHAR, message VARCHAR)`;
5
31
  /**
6
32
  * WHAT AN EVENT IS, one word each: `copy_failed`, `poll_failed`, `catch_up_failed` (the one poll that brings a new copy
7
33
  * current), `stream_failed` (the stream stopped, or refused a table: `table` says which), `source_unreachable` (the
8
- * database could not be attached), `restart` (the supervisor started the run again after it died) — each a failure;
9
- * `copied` (a copy finished and was swapped in), `recovered` (a table whose poll, catch-up or stream failed is
34
+ * database could not be attached), `generated_failed` (a generated table's re-run failed, and it serves what it held),
35
+ * `restart` (the supervisor started the run again after it died) — each a failure;
36
+ * `copied` (a copy finished and was swapped in), `recovered` (a table whose poll, catch-up, stream or re-run failed is
10
37
  * current again; of no table, the stream applying changes or the database attached again after it failed) and
11
38
  * `synced` (a sync asked by hand — mason sync, the page's Sync now — and what it did), and `sync_changed` (a table's
12
39
  * `sync:` in the sources file changed while it is copied: from what, to what, and how its copy is caught up). A scheduled poll that succeeds
13
40
  * is no event: one every few seconds would bury the failures; its last result is the table's last sync.
14
41
  */
15
- export const EVENT_KINDS = ["copy_failed", "poll_failed", "catch_up_failed", "stream_failed", "source_unreachable", "restart", "copied", "recovered", "synced", "sync_changed"];
16
- const FAILURES = new Set(["copy_failed", "poll_failed", "catch_up_failed", "stream_failed", "source_unreachable", "restart"]);
42
+ export const EVENT_KINDS = ["copy_failed", "poll_failed", "catch_up_failed", "stream_failed", "source_unreachable", "generated_failed", "restart", "copied", "recovered", "synced", "sync_changed"];
43
+ const FAILURES = new Set(["copy_failed", "poll_failed", "catch_up_failed", "stream_failed", "source_unreachable", "generated_failed", "restart"]);
17
44
  /** whether an event of this kind is a failure: the one place that says it (the reader's filter, `failed`) */
18
45
  export const isFailure = (kind) => FAILURES.has(kind);
19
46
  const FLUSH_MS = 1000;
@@ -90,9 +117,11 @@ export class SyncEvents {
90
117
  }
91
118
  this.lost.succeeded((n) => this.log(`sync events: recording again; ${n} event(s) were not recorded (the log has them)`));
92
119
  }
93
- /** Deletes the events older than the retention; the count is said when there were any. */
120
+ /** Deletes the events older than the retention; the count is said when there were any. The retention is counted back
121
+ * from the module's ONE clock (`now`, which stamps each event), never the engine's `now()`: two clocks disagree
122
+ * whenever the clock is given, and a trim by the other deleted what the given clock still kept. */
94
123
  async trim() {
95
- const r = await this.history.writer.runAndReadAll(`DELETE FROM ${EVENTS_TABLE} WHERE happened_at < now() - INTERVAL (${this.days}) DAY`);
124
+ const r = await this.history.writer.runAndReadAll(`DELETE FROM ${EVENTS_TABLE} WHERE happened_at < $1::TIMESTAMPTZ`, [cutoff(this.now(), this.days)]);
96
125
  const n = Number(r.getRowsJS()[0]?.[0] ?? 0);
97
126
  if (n > 0)
98
127
  this.log(`sync events: deleted ${n} event(s) older than ${this.days} days (MASON_HISTORY_DAYS)`);
@@ -120,7 +149,9 @@ export class SyncEvents {
120
149
  this.history.closeConnections();
121
150
  }
122
151
  }
123
- const eventOf = (at, kind, table, message) => ({ at, kind, failed: isFailure(kind), table, message });
152
+ /** an event as read back, any `password=` value in its message masked (src/lib/secret-mask.ts `maskPasswordKeyword`,
153
+ * the exact form an earlier Mason leaked, issue 599: the sync scrubs those rows at open, and this holds should one stay) */
154
+ const eventOf = (at, kind, table, message) => ({ at, kind, failed: isFailure(kind), table, message: maskPasswordKeyword(message) });
124
155
  /**
125
156
  * THE EVENTS WITH THE RUN'S RESTARTS (src/cli/supervise.ts `.mason/restarts.json`), as ONE list newest last, asked as
126
157
  * `recent` is (the newest `limit`, since, the failures only — a restart is one), within the retention as the events are.
@@ -14,7 +14,7 @@ import { parentPort, workerData } from "node:worker_threads";
14
14
  import { openDuck, SessionRunner } from "./store.js";
15
15
  import { FUNCTION_CALLS, FUNCTION_ROWS, registerLibcOrder, useCancelTable } from "./functions.js";
16
16
  import { stopPgEval } from "./pg-eval.js";
17
- import { StatementError } from "../api/postgres/server.js";
17
+ import { StatementError } from "./statement.js";
18
18
  import { STOPPING, wireError } from "./executor.js";
19
19
  const { cancels } = workerData;
20
20
  useCancelTable(cancels);
@@ -134,7 +134,7 @@ async function answer(m) {
134
134
  return undefined;
135
135
  }
136
136
  case "libc":
137
- registerLibcOrder(need(conns, 0, "connection"), m.order);
137
+ registerLibcOrder(need(conns, 0, "connection"), String(m.collate), m.order, m.database === true);
138
138
  return { value: null };
139
139
  case "calls": return { value: Object.fromEntries(FUNCTION_CALLS) };
140
140
  case "function rows": return { value: Object.fromEntries(FUNCTION_ROWS) };
@@ -35,7 +35,7 @@ import { Worker } from "node:worker_threads";
35
35
  import { fileURLToPath } from "node:url";
36
36
  import * as duck from "@duckdb/node-api";
37
37
  import { runnable } from "../lib/runnable.js";
38
- import { StatementError } from "../api/postgres/server.js";
38
+ import { StatementError } from "./statement.js";
39
39
  import { EncodeError } from "../api/postgres/encode.js";
40
40
  import { NumericError } from "../lib/pg-numeric.js";
41
41
  import { beats, CANCEL_SLOTS, cancelStatement, cancelTable, resetSlot } from "./cancel.js";
@@ -116,7 +116,7 @@ export function wireError(e) {
116
116
  if (e instanceof EncodeError)
117
117
  return { kind: "encode", message: e.message, code: e.code, gap: e.gap };
118
118
  if (e instanceof NumericError)
119
- return { kind: "numeric", message: e.message, sqlstate: e.sqlstate, gap: e.gap };
119
+ return { kind: "numeric", message: e.message, sqlstate: e.sqlstate, gap: e.gap, detail: e.detail };
120
120
  return { kind: "error", message: e instanceof Error ? e.message : String(e), name: e instanceof Error ? e.name : "Error" };
121
121
  }
122
122
  /** the worker is gone: what every request in flight, and every later one, is answered. `stopped`: by the run's own stop
@@ -135,7 +135,7 @@ const errorOf = (e) => {
135
135
  if (e.kind === "encode")
136
136
  return new EncodeError(e.message, e.code, e.gap);
137
137
  if (e.kind === "numeric")
138
- return new NumericError(e.message, e.sqlstate, e.gap);
138
+ return new NumericError(e.message, e.sqlstate, e.gap, e.detail);
139
139
  const err = new Error(e.message);
140
140
  err.name = e.name;
141
141
  return err;
@@ -368,8 +368,9 @@ export class Executor {
368
368
  /** a connection of the instance */
369
369
  async connect() { return new RemoteConnection(this, await this.call({ op: "connect" })); }
370
370
  writer() { return new RemoteConnection(this, 0); }
371
- /** the libc order's two functions (src/duck/functions.ts registerLibcOrder), registered on the worker */
372
- async registerLibcOrder(order) { await this.call({ op: "libc", order }); }
371
+ /** a libc order's functions (src/duck/functions.ts registerLibcOrder), registered on the worker: the key under
372
+ * `collate`, and jsonb's order under the database's (`database`) */
373
+ async registerLibcOrder(collate, order, database) { await this.call({ op: "libc", collate, order, database }); }
373
374
  /** how many vectors each Mason function was called with so far (src/duck/functions.ts FUNCTION_CALLS: the worker's
374
375
  * count, the functions being its) — what a test or a measurement reads to know whether a statement reached one */
375
376
  async functionCalls() { return new Map(Object.entries(await this.call({ op: "calls" }))); }
@@ -113,6 +113,8 @@ export function keptText(f, now = Date.now(), rows = true) {
113
113
  const k = f.kept;
114
114
  if (k === undefined)
115
115
  return UNKNOWN;
116
+ if (k.by === "generated")
117
+ return k.text;
116
118
  if (k.by === "poll") {
117
119
  const synced = k.syncedAt === null ? "never synced" : `last synced ${agoText(k.syncedAt, now)} (${utc(k.syncedAt)})`;
118
120
  if (k.pending !== undefined)
@@ -144,6 +146,8 @@ export function keptCurrentBy(f) {
144
146
  return k.pending === undefined && k.sync.mode === "poll" ? "polling" : null;
145
147
  if (k.by === "recopy")
146
148
  return "copying again";
149
+ if (k.by === "generated")
150
+ return null;
147
151
  return k.state === "streamed" && k.notRunning === null ? "logical replication" : null;
148
152
  }
149
153
  /** how a copy is kept current, as FORMAT JSON's keys say it (`keptText`'s facts) */