@mason-data/cli 0.8.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.
- package/README.md +13 -4
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +166 -32
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +505 -99
- package/dist/src/api/postgres/translate.js +47 -2
- package/dist/src/cli/background.js +18 -19
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/command.js +169 -11
- package/dist/src/cli/commands.js +207 -51
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +2 -2
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +148 -27
- package/dist/src/cli/doctor.js +116 -33
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +97 -6
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +47 -11
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +1 -1
- package/dist/src/cli/main.js +8 -2
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +2 -2
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +1 -1
- package/dist/src/cli/pin.js +6 -2
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +130 -39
- package/dist/src/cli/project.js +99 -0
- package/dist/src/cli/readiness.js +42 -6
- package/dist/src/cli/registry.js +12 -4
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/roles.js +2 -1
- package/dist/src/cli/run-server.js +154 -7
- package/dist/src/cli/security.js +57 -10
- package/dist/src/cli/serve.js +63 -7
- package/dist/src/cli/share-run.js +51 -8
- package/dist/src/cli/share.js +3 -3
- package/dist/src/cli/source-edit.js +10 -4
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +30 -20
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +15 -23
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +115 -11
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +126 -74
- package/dist/src/demo/seed.js +26 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +39 -8
- package/dist/src/duck/executor-worker.js +2 -2
- package/dist/src/duck/executor.js +6 -5
- package/dist/src/duck/explain.js +4 -0
- package/dist/src/duck/functions.js +448 -49
- package/dist/src/duck/gaps.js +34 -16
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +1 -1
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +4 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +9 -7
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +275 -30
- package/dist/src/duck/publication.js +14 -0
- package/dist/src/duck/requests.js +9 -5
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4567 -583
- package/dist/src/duck/row-filter.js +12 -6
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +294 -68
- package/dist/src/duck/stream.js +9 -5
- package/dist/src/duck/sync.js +602 -126
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +63 -7
- package/dist/src/duck/usage.js +6 -4
- package/dist/src/duck/users.js +5 -0
- package/dist/src/duck/verified.js +159 -2
- package/dist/src/duck/views.js +4 -3
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +13 -4
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +387 -49
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +24 -9
- package/dist/src/web/app.js +9 -4
- package/dist/src/web/pages/status.js +178 -13
- package/dist/src/web/pages/syncs.js +1 -1
- package/dist/src/web/pages/tables.js +8 -3
- package/dist/src/web/pages/users.js +4 -3
- package/dist/src/web/shared.js +19 -3
- 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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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[
|
|
48
|
-
"FROM pg_index i JOIN pg_opclass c ON c.oid = i.indclass[
|
|
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 =
|
|
51
|
-
"AND i.indkey[
|
|
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
|
|
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)
|
package/dist/src/duck/events.js
CHANGED
|
@@ -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), `
|
|
9
|
-
* `
|
|
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()
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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
|
-
/**
|
|
372
|
-
|
|
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" }))); }
|
package/dist/src/duck/explain.js
CHANGED
|
@@ -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) */
|