@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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { functionsCalled } from "./pgtree.js";
|
|
2
|
+
import { BARRIER } from "./row-filter.js";
|
|
3
|
+
/** the run's length in characters: the longest shared prefix a column's values can have and still be told apart by
|
|
4
|
+
* their bounds (a URL's `https://example.com/item/` is 25), measured (docs/DESIGN.md "The libc key for the candidates
|
|
5
|
+
* alone"); a longer one costs each row's bounds more and saves little */
|
|
6
|
+
export const BOUND_CHARS = 48;
|
|
7
|
+
/** the characters a run may hold: printable ASCII (no control character, so `E`, `L` and `H` are none of them) */
|
|
8
|
+
const FIRST = 0x20, LAST = 0x7e;
|
|
9
|
+
/** `L` below `E` below every character a run writes, below `H` */
|
|
10
|
+
const L = 1, E = 2, H = 0x7f;
|
|
11
|
+
const S = (sval) => ({ String: { sval } });
|
|
12
|
+
const str = (s) => ({ A_Const: { sval: { sval: s } } });
|
|
13
|
+
const int = (n) => ({ A_Const: { ival: n === 0 ? {} : { ival: n } } });
|
|
14
|
+
const call = (name, args) => ({ FuncCall: { funcname: [S(name)], args, funcformat: "COERCE_EXPLICIT_CALL" } });
|
|
15
|
+
const op = (name, l, r) => ({ A_Expr: { kind: "AEXPR_OP", name: [S(name)], lexpr: l, rexpr: r } });
|
|
16
|
+
/** an AND of `args`, one AND's own arguments in place, as Postgres's grammar reads `(a AND b) AND c` back */
|
|
17
|
+
const and = (args) => {
|
|
18
|
+
const flat = args.flatMap((a) => isNodeOf(a, "BoolExpr") && bodyOf(a).boolop === "AND_EXPR" ? bodyOf(a).args : [a]);
|
|
19
|
+
return flat.length === 1 ? flat[0] : { BoolExpr: { boolop: "AND_EXPR", args: flat } };
|
|
20
|
+
};
|
|
21
|
+
const nullTest = (arg, type) => ({ NullTest: { arg, nulltesttype: type } });
|
|
22
|
+
const chr = (code) => call("chr", [int(code)]);
|
|
23
|
+
/** a character class of code points, as RE2 reads it: each as `\x{…}`, runs as ranges */
|
|
24
|
+
function charClass(points) {
|
|
25
|
+
const sorted = [...points].sort((a, b) => a - b);
|
|
26
|
+
let out = "";
|
|
27
|
+
for (let i = 0; i < sorted.length;) {
|
|
28
|
+
let j = i;
|
|
29
|
+
while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1)
|
|
30
|
+
j++;
|
|
31
|
+
const hex = (g) => `\\x{${g.toString(16)}}`;
|
|
32
|
+
out += i === j ? hex(sorted[i]) : `${hex(sorted[i])}-${hex(sorted[j])}`;
|
|
33
|
+
i = j + 1;
|
|
34
|
+
}
|
|
35
|
+
return `[${out}]`;
|
|
36
|
+
}
|
|
37
|
+
/** the engine's `lower()` of a printable ASCII character (tests/duck-libc-order.test.ts runs the engine's own) */
|
|
38
|
+
const folded = (g) => g >= 0x41 && g <= 0x5a ? g + 32 : g;
|
|
39
|
+
const BOUNDS = new WeakMap();
|
|
40
|
+
/**
|
|
41
|
+
* The order's bounds, as the module's header says, or null where it has none to give: an order not followed (every
|
|
42
|
+
* value refused, `libc-locale-unread`), byte order, no character mapped, or mapped characters whose weights their
|
|
43
|
+
* folded bytes do not order.
|
|
44
|
+
*/
|
|
45
|
+
export function boundOf(o) {
|
|
46
|
+
const known = BOUNDS.get(o);
|
|
47
|
+
if (known !== undefined)
|
|
48
|
+
return known;
|
|
49
|
+
let bound = null;
|
|
50
|
+
if (!o.unread && !o.bytes) {
|
|
51
|
+
// a character whose first level is its own: covered, listed, no sequence of weights
|
|
52
|
+
const mapped = new Set();
|
|
53
|
+
for (let g = FIRST; g <= LAST; g++)
|
|
54
|
+
if (o.uncovered[g] === 0 && o.unlisted[g] === 0 && !o.sequences.has(g))
|
|
55
|
+
mapped.add(g);
|
|
56
|
+
// a contraction a mapped character could complete makes its first character's weight depend on what follows: not mapped
|
|
57
|
+
for (const g of [...mapped])
|
|
58
|
+
if ((o.contractions.get(g) ?? []).some((c) => c.rest.length > 0 && mapped.has(c.rest[0])))
|
|
59
|
+
mapped.delete(g);
|
|
60
|
+
// (the rest begin a contraction only before an unmapped character: known where a mapped one follows, or the text ends)
|
|
61
|
+
const plain = [...mapped].filter((g) => !o.contractions.has(g));
|
|
62
|
+
const weighted = [...mapped].filter((g) => o.l1[g] > 0), dropped = [...mapped].filter((g) => o.l1[g] === 0);
|
|
63
|
+
// each weighted character folds to a mapped one of its weight, and the folded ones' bytes order as their weights,
|
|
64
|
+
// strictly: one weight, one character written
|
|
65
|
+
const written = [...new Set(weighted.map(folded))].sort((a, b) => a - b);
|
|
66
|
+
const ordered = weighted.every((g) => mapped.has(folded(g)) && o.l1[folded(g)] === o.l1[g])
|
|
67
|
+
&& written.every((g, i) => i === 0 || o.l1[written[i - 1]] < o.l1[g]);
|
|
68
|
+
if (plain.length > 0 && written.length > 0 && ordered) {
|
|
69
|
+
bound = {
|
|
70
|
+
// the longest run of mapped characters ending in one whose weight is known there; and the whole text mapped
|
|
71
|
+
run: `^${charClass([...mapped])}*${charClass(plain)}`,
|
|
72
|
+
whole: `${charClass([...mapped])}*`,
|
|
73
|
+
dropped: dropped.length === 0 ? null : charClass(dropped),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
BOUNDS.set(o, bound);
|
|
78
|
+
return bound;
|
|
79
|
+
}
|
|
80
|
+
/** the run's weights of `x`, ending in `E` where x ended in the run: x of at most `BOUND_CHARS` bytes, each a mapped
|
|
81
|
+
* character (a mapped one is one byte), is whole in it; otherwise the run is the longest prefix of x's first
|
|
82
|
+
* `BOUND_CHARS + 1` characters ending in a plain one. No mark is ever read from x */
|
|
83
|
+
function weightsOf(b, x) {
|
|
84
|
+
const head = () => call("left", [structuredClone(x), int(BOUND_CHARS + 1)]);
|
|
85
|
+
const whole = and([op("<=", call("strlen", [structuredClone(x)]), int(BOUND_CHARS)), call("regexp_full_match", [head(), str(b.whole)])]);
|
|
86
|
+
const run = call("lower", [{ CaseExpr: { args: [{ CaseWhen: { expr: whole, result: op("||", structuredClone(x), chr(E)) } }],
|
|
87
|
+
defresult: call("regexp_extract", [head(), str(b.run)]) } }]);
|
|
88
|
+
return b.dropped === null ? run : call("regexp_replace", [run, str(b.dropped), str(""), str("g")]);
|
|
89
|
+
}
|
|
90
|
+
/** `lo(x)` and `hi(x)` (the module's header), each its own copy of `x` */
|
|
91
|
+
export const lowBound = (b, x) => op("||", weightsOf(b, structuredClone(x)), chr(L));
|
|
92
|
+
export const highBound = (b, x) => op("||", weightsOf(b, structuredClone(x)), chr(H));
|
|
93
|
+
// ------------------------------------------------------------------------------------------------ the statement
|
|
94
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
95
|
+
const kindOf = (n) => isObject(n) ? Object.keys(n)[0] ?? null : null;
|
|
96
|
+
const bodyOf = (n) => n[kindOf(n)];
|
|
97
|
+
const isNodeOf = (n, kind) => kindOf(n) === kind && isObject(bodyOf(n));
|
|
98
|
+
const nameOf = (list) => Array.isArray(list) ? list.map((x) => String(bodyOf(x).sval ?? "")).join(".") : "";
|
|
99
|
+
const present = (v) => v !== undefined && v !== null && !(Array.isArray(v) && v.length === 0);
|
|
100
|
+
/** an integer constant, or null (a 0 is `ival: {}`) */
|
|
101
|
+
const constInt = (n) => {
|
|
102
|
+
if (!isNodeOf(n, "A_Const"))
|
|
103
|
+
return null;
|
|
104
|
+
const v = bodyOf(n).ival;
|
|
105
|
+
return isObject(v) ? Number(v.ival ?? 0) : null;
|
|
106
|
+
};
|
|
107
|
+
/** a libc key the rewrite wrote (`__mason_libc_key(<session>, x, '<collate>')`): x and the collcollate, or null */
|
|
108
|
+
function libcKeyOf(n) {
|
|
109
|
+
if (!isNodeOf(n, "FuncCall") || nameOf(bodyOf(n).funcname) !== "__mason_libc_key")
|
|
110
|
+
return null;
|
|
111
|
+
const args = bodyOf(n).args;
|
|
112
|
+
const collate = args?.at(-1), x = args?.at(-2);
|
|
113
|
+
if (x === undefined || !isNodeOf(collate, "A_Const") || !isObject(bodyOf(collate).sval))
|
|
114
|
+
return null;
|
|
115
|
+
return { x, collate: String(bodyOf(collate).sval.sval ?? "") };
|
|
116
|
+
}
|
|
117
|
+
/** every SELECT body inside `v`, at any depth (a subquery's, a derived table's, a WITH query's) */
|
|
118
|
+
function selectsIn(v, out = []) {
|
|
119
|
+
if (Array.isArray(v)) {
|
|
120
|
+
for (const x of v)
|
|
121
|
+
selectsIn(x, out);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
if (!isObject(v))
|
|
125
|
+
return out;
|
|
126
|
+
if (isNodeOf(v, "SelectStmt"))
|
|
127
|
+
out.push(bodyOf(v));
|
|
128
|
+
for (const x of Object.values(v))
|
|
129
|
+
selectsIn(x, out);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** `v` holds a node of one of `kinds` */
|
|
133
|
+
function holds(v, kinds) {
|
|
134
|
+
if (Array.isArray(v))
|
|
135
|
+
return v.some((x) => holds(x, kinds));
|
|
136
|
+
if (!isObject(v))
|
|
137
|
+
return false;
|
|
138
|
+
const k = kindOf(v);
|
|
139
|
+
if (k !== null && kinds.has(k) && isObject(v[k]))
|
|
140
|
+
return true;
|
|
141
|
+
return Object.values(v).some((x) => holds(x, kinds));
|
|
142
|
+
}
|
|
143
|
+
/** a role's barrier (src/duck/row-filter.ts `BARRIER`): a LIMIT every row passes */
|
|
144
|
+
const BARRIER_COUNT = /\d+/.exec(BARRIER)[0];
|
|
145
|
+
const isBarrier = (s) => {
|
|
146
|
+
const c = s.limitCount;
|
|
147
|
+
return isNodeOf(c, "A_Const") && String(bodyOf(c).fval?.fval ?? bodyOf(c).ival?.ival ?? "") === BARRIER_COUNT && (s.limitOffset === undefined || constInt(s.limitOffset) === 0);
|
|
148
|
+
};
|
|
149
|
+
/** what is cloned reads the same rows twice: no LIMIT or OFFSET but a barrier, no DISTINCT ON, no sample, in any SELECT
|
|
150
|
+
* inside it (nothing volatile, no window and no set-returning list in the statement: `statementReads`) */
|
|
151
|
+
function readsAlike(v) {
|
|
152
|
+
if (holds(v, new Set(["RangeTableSample"])))
|
|
153
|
+
return false;
|
|
154
|
+
return selectsIn(v).every((s) => ((s.limitCount === undefined && s.limitOffset === undefined) || isBarrier(s))
|
|
155
|
+
&& !(Array.isArray(s.distinctClause) && s.distinctClause.some((d) => kindOf(d) !== null)));
|
|
156
|
+
}
|
|
157
|
+
/** the WITH queries of the statement, by name (any depth: a name read in FROM may be any of them) */
|
|
158
|
+
function withQueries(tree) {
|
|
159
|
+
const out = new Map();
|
|
160
|
+
for (const s of selectsIn(tree)) {
|
|
161
|
+
for (const c of (s.withClause?.ctes ?? [])) {
|
|
162
|
+
if (!isNodeOf(c, "CommonTableExpr"))
|
|
163
|
+
continue;
|
|
164
|
+
const b = bodyOf(c);
|
|
165
|
+
out.set(String(b.ctename), [...out.get(String(b.ctename)) ?? [], b.ctequery]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
/** the one FROM item of a SELECT that may be read twice: a table, a derived table, a WITH query, as `readsAlike` says */
|
|
171
|
+
function oneItem(s, ctes) {
|
|
172
|
+
const from = s.fromClause;
|
|
173
|
+
if (from?.length !== 1)
|
|
174
|
+
return null;
|
|
175
|
+
const item = from[0];
|
|
176
|
+
if (isNodeOf(item, "RangeVar")) {
|
|
177
|
+
const b = bodyOf(item);
|
|
178
|
+
const named = b.schemaname === undefined ? ctes.get(String(b.relname)) : undefined;
|
|
179
|
+
return named === undefined || named.every(readsAlike) ? item : null;
|
|
180
|
+
}
|
|
181
|
+
if (isNodeOf(item, "RangeSubselect") && bodyOf(item).lateral !== true && readsAlike(item))
|
|
182
|
+
return item;
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function statementReads(tree) {
|
|
186
|
+
let aggregates = 0, windowsOrSets = false;
|
|
187
|
+
const visit = (v) => {
|
|
188
|
+
if (Array.isArray(v)) {
|
|
189
|
+
for (const x of v)
|
|
190
|
+
visit(x);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (v === null || v === undefined || typeof v !== "object")
|
|
194
|
+
return;
|
|
195
|
+
const n = v;
|
|
196
|
+
if (n._node === "QUERY") {
|
|
197
|
+
if (n.hasAggs !== "false")
|
|
198
|
+
aggregates++;
|
|
199
|
+
if (n.hasWindowFuncs !== "false" || n.hasTargetSRFs !== "false")
|
|
200
|
+
windowsOrSets = true;
|
|
201
|
+
}
|
|
202
|
+
for (const [k, x] of Object.entries(n))
|
|
203
|
+
if (k !== "_node")
|
|
204
|
+
visit(x);
|
|
205
|
+
};
|
|
206
|
+
visit(tree);
|
|
207
|
+
return { aggregates, windowsOrSets };
|
|
208
|
+
}
|
|
209
|
+
/** a SELECT that is no set operation, lock or INTO, and whose list holds no window */
|
|
210
|
+
const plainSelect = (s) => (s.op === undefined || s.op === "SETOP_NONE") && !present(s.windowClause) && !present(s.lockingClause)
|
|
211
|
+
&& !present(s.intoClause) && !present(s.valuesLists) && !windowed(s.targetList);
|
|
212
|
+
/** a call OVER a window in `v`, outside its subqueries */
|
|
213
|
+
function windowed(v) {
|
|
214
|
+
if (Array.isArray(v))
|
|
215
|
+
return v.some(windowed);
|
|
216
|
+
if (!isObject(v) || isNodeOf(v, "SubLink"))
|
|
217
|
+
return false;
|
|
218
|
+
if (isNodeOf(v, "FuncCall") && bodyOf(v).over !== undefined)
|
|
219
|
+
return true;
|
|
220
|
+
return Object.values(v).some(windowed);
|
|
221
|
+
}
|
|
222
|
+
/** the threshold's subquery over the SELECT's own rows: `SELECT <aggregate> FROM (SELECT <value> AS __mason_bound FROM
|
|
223
|
+
* <its FROM item> WHERE <its WHERE> AND x IS NOT NULL [ORDER BY 1 [DESC] LIMIT n]) AS __mason_bounds`; a top N's is its
|
|
224
|
+
* `everyRow` where fewer than n rows have a value (`CASE WHEN count(*) >= n THEN … ELSE <everyRow> END`) */
|
|
225
|
+
function threshold(s, item, x, value, aggregate, top) {
|
|
226
|
+
const where = and([...(s.whereClause !== undefined ? [structuredClone(s.whereClause)] : []), nullTest(structuredClone(x), "IS_NOT_NULL")]);
|
|
227
|
+
const inner = {
|
|
228
|
+
targetList: [{ ResTarget: { name: "__mason_bound", val: value } }],
|
|
229
|
+
fromClause: [structuredClone(item)], whereClause: where,
|
|
230
|
+
...(top === null ? {} : {
|
|
231
|
+
sortClause: [{ SortBy: { node: int(1), sortby_dir: top.desc ? "SORTBY_DESC" : "SORTBY_DEFAULT", sortby_nulls: "SORTBY_NULLS_DEFAULT" } }],
|
|
232
|
+
limitCount: int(top.n), limitOption: "LIMIT_OPTION_COUNT",
|
|
233
|
+
}),
|
|
234
|
+
op: "SETOP_NONE",
|
|
235
|
+
};
|
|
236
|
+
const outer = {
|
|
237
|
+
targetList: [{ ResTarget: { val: ((v) => top === null ? v : { CaseExpr: {
|
|
238
|
+
args: [{ CaseWhen: { expr: op(">=", { FuncCall: { funcname: [S("count")], agg_star: true, funcformat: "COERCE_EXPLICIT_CALL" } }, int(top.n)), result: v } }], defresult: top.everyRow
|
|
239
|
+
} })(call(aggregate, [{ ColumnRef: { fields: [S("__mason_bounds"), S("__mason_bound")] } }])) } }],
|
|
240
|
+
fromClause: [{ RangeSubselect: { subquery: { SelectStmt: inner }, alias: { aliasname: "__mason_bounds" } } }],
|
|
241
|
+
op: "SETOP_NONE",
|
|
242
|
+
};
|
|
243
|
+
return { SubLink: { subLinkType: "EXPR_SUBLINK", subselect: { SelectStmt: outer } } };
|
|
244
|
+
}
|
|
245
|
+
/** a role's walled reads in `v`: each `SELECT * FROM (… LIMIT <barrier>) AS <wall>` */
|
|
246
|
+
const wallsIn = (v, alias) => selectsIn(v).filter((s) => {
|
|
247
|
+
const from = s.fromClause;
|
|
248
|
+
return from?.length === 1 && isNodeOf(from[0], "RangeSubselect") && bodyOf(from[0]).alias?.aliasname === alias;
|
|
249
|
+
}).length;
|
|
250
|
+
/**
|
|
251
|
+
* The statement with each top N and each ungrouped min / max under a libc order keyed for its candidates alone (the
|
|
252
|
+
* module's header), or null where none is. `tree` is the rewrite's DuckDB tree (after src/duck/late.ts), `analysis`
|
|
253
|
+
* Postgres's of the statement it was written from.
|
|
254
|
+
*/
|
|
255
|
+
export async function libcBounded(tree, analysis, context) {
|
|
256
|
+
const tops = [], picks = [];
|
|
257
|
+
const ctes = withQueries(tree);
|
|
258
|
+
const boundFor = (collate) => { const o = context.order(collate); return o === undefined ? null : boundOf(o); };
|
|
259
|
+
for (const s of selectsIn(tree)) {
|
|
260
|
+
if (!plainSelect(s) || oneItem(s, ctes) === null || (s.whereClause !== undefined && !readsAlike(s.whereClause)))
|
|
261
|
+
continue;
|
|
262
|
+
// A TOP N: the first key a libc key, a constant LIMIT and OFFSET, no DISTINCT or GROUP BY
|
|
263
|
+
const sorts = (s.sortClause ?? []);
|
|
264
|
+
const first = sorts[0] !== undefined && isNodeOf(sorts[0], "SortBy") ? bodyOf(sorts[0]) : null;
|
|
265
|
+
const key = first === null || first.useOp !== undefined ? null : libcKeyOf(first.node);
|
|
266
|
+
const count = constInt(s.limitCount), offset = s.limitOffset === undefined ? 0 : constInt(s.limitOffset);
|
|
267
|
+
if (key !== null && count !== null && count > 0 && offset !== null && offset >= 0 && count + offset <= 2_147_483_647 && (s.limitOption === undefined || s.limitOption === "LIMIT_OPTION_COUNT" || s.limitOption === "LIMIT_OPTION_DEFAULT")
|
|
268
|
+
&& !present(s.distinctClause) && !present(s.groupClause) && !present(s.havingClause)) {
|
|
269
|
+
const bound = boundFor(key.collate);
|
|
270
|
+
// (where NULL sorts: the engine's default_null_order is Postgres's, last ascending and first descending)
|
|
271
|
+
const desc = first.sortby_dir === "SORTBY_DESC";
|
|
272
|
+
const nullsFirst = first.sortby_nulls === "SORTBY_NULLS_FIRST" || (desc && first.sortby_nulls !== "SORTBY_NULLS_LAST");
|
|
273
|
+
if (bound !== null)
|
|
274
|
+
tops.push({ s, x: key.x, bound, desc, nullsFirst, n: count + offset });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// A MIN OR A MAX: arg_min / arg_max over the libc key's CASE (src/duck/rewrite.ts textMinMax), ungrouped, no FILTER
|
|
278
|
+
if (present(s.groupClause) || present(s.distinctClause) || present(s.sortClause) || s.limitCount !== undefined || s.limitOffset !== undefined)
|
|
279
|
+
continue;
|
|
280
|
+
const each = [];
|
|
281
|
+
const visit = (v) => {
|
|
282
|
+
if (Array.isArray(v)) {
|
|
283
|
+
v.forEach(visit);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (!isObject(v) || isNodeOf(v, "SubLink"))
|
|
287
|
+
return;
|
|
288
|
+
if (isNodeOf(v, "FuncCall")) {
|
|
289
|
+
const f = bodyOf(v), name = nameOf(f.funcname), args = f.args;
|
|
290
|
+
const arm = args?.[1] !== undefined && isNodeOf(args[1], "CaseExpr") ? bodyOf(args[1]) : null;
|
|
291
|
+
const when = arm !== null && arm.args.length === 1 && arm.defresult === undefined ? bodyOf(arm.args[0]) : null;
|
|
292
|
+
const result = when !== null && isNodeOf(when.result, "RowExpr") ? bodyOf(when.result).args[0] : undefined;
|
|
293
|
+
const k = libcKeyOf(result);
|
|
294
|
+
if ((name === "arg_min" || name === "arg_max") && k !== null && f.over === undefined && f.agg_filter === undefined && f.agg_distinct !== true) {
|
|
295
|
+
const bound = boundFor(k.collate);
|
|
296
|
+
if (bound !== null)
|
|
297
|
+
each.push({ case: when, x: k.x, bound, min: name === "arg_min" });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
Object.values(v).forEach(visit);
|
|
302
|
+
};
|
|
303
|
+
visit(s.targetList);
|
|
304
|
+
if (each.length > 0)
|
|
305
|
+
picks.push({ s, each });
|
|
306
|
+
}
|
|
307
|
+
if (tops.length === 0 && picks.length === 0)
|
|
308
|
+
return null;
|
|
309
|
+
// the statement reads its rows alike twice (the module's header): Postgres's analysis and pg_proc decide it
|
|
310
|
+
const reads = statementReads(analysis);
|
|
311
|
+
if (reads.windowsOrSets)
|
|
312
|
+
return null;
|
|
313
|
+
if ((await context.volatile([...functionsCalled(analysis).oids].filter((f) => f > 0).sort((a, b) => a - b))).size > 0)
|
|
314
|
+
return null;
|
|
315
|
+
// a top N in a statement of no aggregate; a min or max in the one query that aggregates
|
|
316
|
+
const chosen = { tops: reads.aggregates === 0 ? tops : [], picks: reads.aggregates === 1 ? picks : [] };
|
|
317
|
+
if (chosen.tops.length === 0 && chosen.picks.length === 0)
|
|
318
|
+
return null;
|
|
319
|
+
const counted = (n) => { if (context.wall !== null)
|
|
320
|
+
context.wall.added += wallsIn(n, context.wall.alias); return n; };
|
|
321
|
+
for (const t of chosen.tops) {
|
|
322
|
+
const item = oneItem(t.s, ctes);
|
|
323
|
+
// ASC: the n-th smallest hi, rows of lo <= it; DESC: the n-th largest lo, rows of hi >= it. Fewer than n values,
|
|
324
|
+
// a threshold every bound passes: `H` above every lo, '' below every hi. A NULL row's bound is one every threshold
|
|
325
|
+
// passes where NULL sorts first, and else the one only that threshold passes
|
|
326
|
+
const [everyRow, beforeEvery] = t.desc ? [str(""), chr(H)] : [chr(H), str("")];
|
|
327
|
+
const limit = counted(threshold(t.s, item, t.x, (t.desc ? lowBound : highBound)(t.bound, t.x), t.desc ? "min" : "max", { desc: t.desc, n: t.n, everyRow }));
|
|
328
|
+
const own = { CoalesceExpr: { args: [(t.desc ? highBound : lowBound)(t.bound, t.x), t.nullsFirst ? beforeEvery : structuredClone(everyRow)] } };
|
|
329
|
+
const kept = op(t.desc ? ">=" : "<=", own, limit);
|
|
330
|
+
t.s.whereClause = t.s.whereClause === undefined ? kept : and([t.s.whereClause, kept]);
|
|
331
|
+
}
|
|
332
|
+
for (const { s, each } of chosen.picks) {
|
|
333
|
+
const item = oneItem(s, ctes);
|
|
334
|
+
for (const p of each) {
|
|
335
|
+
// min: the least hi, rows of lo <= it; max: the greatest lo, rows of hi >= it
|
|
336
|
+
const limit = counted(threshold(s, item, p.x, (p.min ? highBound : lowBound)(p.bound, p.x), p.min ? "min" : "max", null));
|
|
337
|
+
p.case.expr = and([p.case.expr, op(p.min ? "<=" : ">=", (p.min ? lowBound : highBound)(p.bound, p.x), limit)]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return tree;
|
|
341
|
+
}
|