@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
package/dist/src/duck/pgtree.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `(o …)` OIDs, `(i …)` ints), `<>` for NULL, a quoted string, a bare token (outToken backslash-escapes the
|
|
13
13
|
* characters that would end one), or a datum `N [ b b … ]` (a Const's value).
|
|
14
14
|
*/
|
|
15
|
+
import { constantExpression } from "./constant-key.js";
|
|
15
16
|
/** Parses one node-format text (outfuncs.c's nodeToString), or throws with the offset where it went wrong. */
|
|
16
17
|
export function parseTree(text) {
|
|
17
18
|
let i = 0;
|
|
@@ -108,8 +109,68 @@ export function parseTree(text) {
|
|
|
108
109
|
fail("text after the tree");
|
|
109
110
|
return v;
|
|
110
111
|
}
|
|
112
|
+
/** Postgres's first OID for an object not of its own (FirstNormalObjectId): below it, Postgres's catalog; past it, a
|
|
113
|
+
* table copied, a type an extension made, or what the catalog Postgres made itself (catalog.ts LATEST_SQL) */
|
|
114
|
+
export const FIRST_USER_OID = 16384;
|
|
111
115
|
const isNode = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
112
116
|
const num = (v) => typeof v === "string" && /^-?\d+$/.test(v) ? Number(v) : null;
|
|
117
|
+
/** the integers of an integer list (`(i 2 3)`) */
|
|
118
|
+
const ints = (v) => Array.isArray(v) ? v.map((x) => typeof x === "string" ? num(x) : null).filter((x) => x !== null) : [];
|
|
119
|
+
/** each GROUPINGFUNC of an analysed expression, in the order a walk of it meets them, outside its subqueries */
|
|
120
|
+
function groupingsIn(v) {
|
|
121
|
+
if (Array.isArray(v))
|
|
122
|
+
return v.flatMap(groupingsIn);
|
|
123
|
+
if (v === undefined || !isNode(v) || v._node === "QUERY")
|
|
124
|
+
return [];
|
|
125
|
+
const own = v._node === "GROUPINGFUNC" ? [{ levelsUp: num(v.agglevelsup ?? null) ?? 0, refs: ints(v.refs) }] : [];
|
|
126
|
+
return [...own, ...Object.entries(v).flatMap(([k, x]) => k === "_node" ? [] : groupingsIn(x))];
|
|
127
|
+
}
|
|
128
|
+
/** Keys: `targets` a TARGETENTRY list, `sort` and `distinct` SORTGROUPCLAUSE lists over it */
|
|
129
|
+
function keysOf(targets, sort, distinct, rtable, windows) {
|
|
130
|
+
const list = Array.isArray(targets) ? targets.filter(isNode) : [];
|
|
131
|
+
// (a grouped key's entry is a VAR of the GROUP entry by the time the tree is printed; it was bound as what it groups)
|
|
132
|
+
const exprs = list.map((t) => ungrouped(t.expr ?? null, rtable));
|
|
133
|
+
const entries = exprs.map((e, i) => ({
|
|
134
|
+
constant: constantExpression(e),
|
|
135
|
+
at: isNode(e) ? num(e.location) : null,
|
|
136
|
+
first: exprs.findIndex((o, j) => j <= i && sameExpression(o, e)),
|
|
137
|
+
reads: readsVar(e),
|
|
138
|
+
output: list[i].resjunk !== "true",
|
|
139
|
+
ref: num(list[i].ressortgroupref ?? null),
|
|
140
|
+
groupings: groupingsIn(list[i].expr ?? null),
|
|
141
|
+
}));
|
|
142
|
+
const refs = (v) => (Array.isArray(v) ? v.filter(isNode) : []).map((c) => {
|
|
143
|
+
const ref = num(c.tleSortGroupRef ?? null);
|
|
144
|
+
return list.findIndex((t) => ref !== null && ref !== 0 && num(t.ressortgroupref ?? null) === ref);
|
|
145
|
+
});
|
|
146
|
+
const clauses = Array.isArray(windows) ? windows.filter(isNode) : [];
|
|
147
|
+
return {
|
|
148
|
+
entries, sorted: refs(sort), distinct: refs(distinct),
|
|
149
|
+
windows: clauses.map((w) => ({ name: typeof w.name === "string" ? w.name : null, winref: num(w.winref ?? null), partition: refs(w.partitionClause), order: refs(w.orderClause) })),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** whether an analysed expression reads a column: a VAR outside a subquery's own QUERY */
|
|
153
|
+
function readsVar(v) {
|
|
154
|
+
if (Array.isArray(v))
|
|
155
|
+
return v.some(readsVar);
|
|
156
|
+
if (v === undefined || !isNode(v) || v._node === "QUERY")
|
|
157
|
+
return false;
|
|
158
|
+
return v._node === "VAR" || Object.entries(v).some(([k, x]) => k !== "_node" && readsVar(x));
|
|
159
|
+
}
|
|
160
|
+
/** Postgres's equal() of two analysed expressions: alike in every field but an offset and a cast's form (`::` or an
|
|
161
|
+
* implicit cast, which its equal() ignores too: COMPARE_COERCIONFORM_FIELD) */
|
|
162
|
+
const IGNORED = new Set(["location", "funcformat", "relabelformat", "coerceformat", "convertformat", "row_format"]);
|
|
163
|
+
function sameExpression(a, b) {
|
|
164
|
+
if (a === b)
|
|
165
|
+
return true;
|
|
166
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
167
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x, i) => sameExpression(x, b[i]));
|
|
168
|
+
}
|
|
169
|
+
if (a === undefined || b === undefined || !isNode(a) || !isNode(b))
|
|
170
|
+
return false;
|
|
171
|
+
const ka = Object.keys(a).filter((k) => !IGNORED.has(k)), kb = Object.keys(b).filter((k) => !IGNORED.has(k));
|
|
172
|
+
return ka.length === kb.length && ka.every((k) => sameExpression(a[k], b[k]));
|
|
173
|
+
}
|
|
113
174
|
/** a `constvalue`'s bytes: `<length> [ b0 b1 … ]`, each a signed byte */
|
|
114
175
|
export function datumBytes(v) {
|
|
115
176
|
if (typeof v !== "string")
|
|
@@ -117,6 +178,84 @@ export function datumBytes(v) {
|
|
|
117
178
|
const m = /^\s*\d+\s*\[([-\d\s]*)\]\s*$/.exec(v);
|
|
118
179
|
return m ? m[1].trim().split(/\s+/).filter((x) => x !== "").map((x) => Number(x) & 255) : null;
|
|
119
180
|
}
|
|
181
|
+
const [TEXT, VARCHAR, TEXT_ARRAY, VARCHAR_ARRAY] = [25, 1043, 1009, 1015];
|
|
182
|
+
const int32 = (b, at) => ((b[at] ?? 0) | ((b[at + 1] ?? 0) << 8) | ((b[at + 2] ?? 0) << 16) | ((b[at + 3] ?? 0) << 24)) >>> 0;
|
|
183
|
+
/** a varlena's text and the bytes it takes, in a datum's bytes at `at`; null for a form no constant is written in
|
|
184
|
+
* (compressed, external), which the caller reads as no constant */
|
|
185
|
+
function varlena(b, at) {
|
|
186
|
+
const first = b[at];
|
|
187
|
+
if (first === undefined)
|
|
188
|
+
return null;
|
|
189
|
+
// a 1-byte header (low bit 1) holds its length, itself included, in the seven bits above; 0x01 is an external pointer
|
|
190
|
+
const short = (first & 1) === 1;
|
|
191
|
+
if (first === 1 || (!short && (first & 3) !== 0))
|
|
192
|
+
return null;
|
|
193
|
+
const size = short ? first >>> 1 : int32(b, at) >>> 2, head = short ? 1 : 4;
|
|
194
|
+
if (size < head || at + size > b.length)
|
|
195
|
+
return null;
|
|
196
|
+
return { text: Buffer.from(b.slice(at + head, at + size)).toString("utf8"), size };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A text[] or varchar[] datum's elements as `array_in` read them (array.h: the varlena header, ndim, dataoffset,
|
|
200
|
+
* elemtype, the dims and lower bounds, the null bitmap where dataoffset is set, then the elements, each aligned to 4
|
|
201
|
+
* bytes; the catalog is little-endian). Null for what is not a plain array of at most one dimension.
|
|
202
|
+
*/
|
|
203
|
+
export function textArrayElements(b) {
|
|
204
|
+
if (b.length < 16)
|
|
205
|
+
return null;
|
|
206
|
+
const ndim = int32(b, 4), offset = int32(b, 8), element = int32(b, 12);
|
|
207
|
+
if (ndim > 1 || (element !== TEXT && element !== VARCHAR))
|
|
208
|
+
return null;
|
|
209
|
+
if (ndim === 0)
|
|
210
|
+
return [];
|
|
211
|
+
if (b.length < 24)
|
|
212
|
+
return null;
|
|
213
|
+
const count = int32(b, 16), bitmap = 24;
|
|
214
|
+
let at = offset !== 0 ? offset : 24;
|
|
215
|
+
const out = [];
|
|
216
|
+
for (let i = 0; i < count; i++) {
|
|
217
|
+
if (offset !== 0 && (((b[bitmap + (i >> 3)] ?? 0) >> (i & 7)) & 1) === 0) {
|
|
218
|
+
out.push(null);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const v = varlena(b, at);
|
|
222
|
+
if (v === null)
|
|
223
|
+
return null;
|
|
224
|
+
out.push(v.text);
|
|
225
|
+
at = (at + v.size + 3) & ~3;
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
/** a CONST of text or varchar as Postgres read it; undefined for any other node */
|
|
230
|
+
function constText(v) {
|
|
231
|
+
if (!isNode(v) || v._node !== "CONST" || (num(v.consttype) !== TEXT && num(v.consttype) !== VARCHAR))
|
|
232
|
+
return undefined;
|
|
233
|
+
if (v.constisnull === "true")
|
|
234
|
+
return null;
|
|
235
|
+
const b = datumBytes(v.constvalue);
|
|
236
|
+
return (b === null ? null : varlena(b, 0))?.text;
|
|
237
|
+
}
|
|
238
|
+
/** a text[] or varchar[] that is constant, as Postgres read it; undefined for any other node */
|
|
239
|
+
function constTextArray(v) {
|
|
240
|
+
if (v._node === "CONST") {
|
|
241
|
+
if (num(v.consttype) !== TEXT_ARRAY && num(v.consttype) !== VARCHAR_ARRAY)
|
|
242
|
+
return undefined;
|
|
243
|
+
if (v.constisnull === "true")
|
|
244
|
+
return null;
|
|
245
|
+
const b = datumBytes(v.constvalue);
|
|
246
|
+
return (b === null ? null : textArrayElements(b)) ?? undefined;
|
|
247
|
+
}
|
|
248
|
+
if (v._node !== "ARRAYEXPR" || v.multidims === "true" || (num(v.array_typeid) !== TEXT_ARRAY && num(v.array_typeid) !== VARCHAR_ARRAY))
|
|
249
|
+
return undefined;
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const e of Array.isArray(v.elements) ? v.elements : []) {
|
|
252
|
+
const text = constText(e);
|
|
253
|
+
if (text === undefined)
|
|
254
|
+
return undefined;
|
|
255
|
+
out.push(text);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
120
259
|
const BOOL = 16;
|
|
121
260
|
/** The type an expression node yields: exprType (nodeFuncs.c), for the nodes a SELECT's expressions are made of. */
|
|
122
261
|
export function exprType(n) {
|
|
@@ -130,6 +269,7 @@ export function exprType(n) {
|
|
|
130
269
|
case "PARAM": return t("paramtype", "paramtypmod");
|
|
131
270
|
case "AGGREF": return t("aggtype");
|
|
132
271
|
case "WINDOWFUNC": return t("wintype");
|
|
272
|
+
case "GROUPINGFUNC": return { type: 23, typmod: -1 };
|
|
133
273
|
case "FUNCEXPR": {
|
|
134
274
|
const r = t("funcresulttype");
|
|
135
275
|
// a length coercion (numeric(p, s), varchar(n)) carries its typmod as its second argument's constant
|
|
@@ -148,7 +288,8 @@ export function exprType(n) {
|
|
|
148
288
|
case "SCALARARRAYOPEXPR":
|
|
149
289
|
case "BOOLEXPR":
|
|
150
290
|
case "NULLTEST":
|
|
151
|
-
case "BOOLEANTEST":
|
|
291
|
+
case "BOOLEANTEST":
|
|
292
|
+
case "ROWCOMPAREEXPR": return { type: BOOL, typmod: -1 };
|
|
152
293
|
case "RELABELTYPE":
|
|
153
294
|
case "COERCEVIAIO":
|
|
154
295
|
case "ARRAYCOERCEEXPR":
|
|
@@ -190,6 +331,10 @@ const argsOf = (n) => {
|
|
|
190
331
|
*/
|
|
191
332
|
export function typesByLocation(tree) {
|
|
192
333
|
const out = new Map();
|
|
334
|
+
// the arguments Postgres wrapped in its implicit time→timetz cast (`Typed.implicitTimetz`)
|
|
335
|
+
const asTimetz = new WeakSet();
|
|
336
|
+
// the char(n) arguments Postgres wrapped in its implicit cast to text or varchar (`Typed.implicitText`)
|
|
337
|
+
const asText = new WeakMap();
|
|
193
338
|
// the range tables of the queries around the node being visited, innermost last
|
|
194
339
|
const rtables = [];
|
|
195
340
|
const visit = (v) => {
|
|
@@ -212,45 +357,516 @@ export function typesByLocation(tree) {
|
|
|
212
357
|
}
|
|
213
358
|
return;
|
|
214
359
|
}
|
|
215
|
-
|
|
360
|
+
// a row comparison has no offset of its own: it is found at its first left column's (`rowCompareAt`)
|
|
361
|
+
const first = v._node === "ROWCOMPAREEXPR" && Array.isArray(v.largs) && isNode(v.largs[0] ?? null) ? v.largs[0] : null;
|
|
362
|
+
const placed = (x) => {
|
|
363
|
+
const at = num(x.location ?? null);
|
|
364
|
+
// (an implicit cast Postgres added has none of its own: its argument's)
|
|
365
|
+
return (at === null || at < 0) && isNode(x.arg ?? null) ? placed(x.arg) : at;
|
|
366
|
+
};
|
|
367
|
+
const loc = first !== null ? placed(first) : num(v.location);
|
|
216
368
|
const t = exprType(v);
|
|
369
|
+
const wrapped = implicitTimetzArg(v);
|
|
370
|
+
if (wrapped !== null)
|
|
371
|
+
asTimetz.add(wrapped);
|
|
372
|
+
const trimmed = implicitTextArg(v);
|
|
373
|
+
if (trimmed !== null)
|
|
374
|
+
asText.set(trimmed, exprType(v).type);
|
|
217
375
|
if (loc !== null && loc >= 0 && t !== null) {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
oid: num(v.opno ?? v.funcid ?? v.aggfnoid ?? v.winfnoid ?? null),
|
|
225
|
-
format: num(v.funcformat ?? null),
|
|
226
|
-
collation: num(v.opcollid ?? v.funccollid ?? v.aggcollid ?? v.wincollid ?? v.varcollid ?? v.constcollid ?? v.collid ?? null) ?? 0,
|
|
227
|
-
inputCollation: num(v.inputcollid ?? null) ?? 0,
|
|
228
|
-
levelsUp: v._node === "VAR" ? num(v.varlevelsup ?? null) : null,
|
|
229
|
-
datum: v._node === "CONST" && v.constisnull !== "true" ? datumBytes(v.constvalue) : null,
|
|
230
|
-
qualifier: v._node === "VAR" ? qualifierOf(v, rtables) : null,
|
|
231
|
-
renamed: v._node === "VAR" ? renamedOf(v, rtables) : null,
|
|
232
|
-
};
|
|
233
|
-
const relation = v._node === "VAR" ? relationOf(v, rtables) : null;
|
|
234
|
-
if (relation !== null)
|
|
235
|
-
typed.relation = relation;
|
|
236
|
-
const fromFunction = v._node === "VAR" ? functionColumnOf(v, rtables) : null;
|
|
237
|
-
if (fromFunction !== null)
|
|
238
|
-
typed.functionColumn = fromFunction;
|
|
376
|
+
const typed = typedOf(v, t, rtables);
|
|
377
|
+
if (asTimetz.has(v))
|
|
378
|
+
typed.implicitTimetz = true;
|
|
379
|
+
const text = asText.get(v);
|
|
380
|
+
if (text !== undefined)
|
|
381
|
+
typed.implicitText = text;
|
|
239
382
|
const list = out.get(loc);
|
|
240
383
|
if (list)
|
|
241
384
|
list.push(typed);
|
|
242
385
|
else
|
|
243
386
|
out.set(loc, [typed]);
|
|
244
387
|
}
|
|
388
|
+
// a row compared with a subquery's rows is found at its SubLink's offset too: a grouped column's Var has none
|
|
389
|
+
// (PR 674 class round r3: `(id, a) < ANY (…) … GROUP BY id` placed nothing at its first column)
|
|
390
|
+
const test = v._node === "SUBLINK" && isNode(v.testexpr ?? null) ? v.testexpr : null;
|
|
391
|
+
const at = num(v.location);
|
|
392
|
+
if (test !== null && test._node === "ROWCOMPAREEXPR" && at !== null && at >= 0) {
|
|
393
|
+
const typed = typedOf(test, { type: BOOL, typmod: -1 }, rtables);
|
|
394
|
+
const list = out.get(at);
|
|
395
|
+
if (list)
|
|
396
|
+
list.push(typed);
|
|
397
|
+
else
|
|
398
|
+
out.set(at, [typed]);
|
|
399
|
+
}
|
|
400
|
+
for (const [k, x] of Object.entries(v))
|
|
401
|
+
if (k !== "_node")
|
|
402
|
+
visit(x);
|
|
403
|
+
};
|
|
404
|
+
visit(tree);
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
/** the argument of Postgres's IMPLICIT cast of a char(n) to a text or a varchar (a FUNCEXPR whose one argument is a
|
|
408
|
+
* bpchar and whose result a text or a varchar, COERCE_IMPLICIT_CAST: text(bpchar), which drops the trailing blanks),
|
|
409
|
+
* or null for any other node */
|
|
410
|
+
function implicitTextArg(v) {
|
|
411
|
+
if (v._node !== "FUNCEXPR" || num(v.funcformat ?? null) !== 2)
|
|
412
|
+
return null;
|
|
413
|
+
const to = num(v.funcresulttype ?? null);
|
|
414
|
+
if (to !== 25 && to !== 1043)
|
|
415
|
+
return null;
|
|
416
|
+
const args = argsOf(v).filter(isNode);
|
|
417
|
+
const a = args[0];
|
|
418
|
+
return args.length === 1 && a !== undefined && a._node !== "CASETESTEXPR" && exprType(a)?.type === 1042 ? a : null;
|
|
419
|
+
}
|
|
420
|
+
/** a SubLink's output column (PARAM_SUBLINK) an argument holds, beneath any coercion, its type as the subquery gave it */
|
|
421
|
+
function sublinkParamOf(v) {
|
|
422
|
+
if (!isNode(v))
|
|
423
|
+
return null;
|
|
424
|
+
if (v._node === "PARAM")
|
|
425
|
+
return num(v.paramkind ?? null) === 2 ? exprType(v) : null;
|
|
426
|
+
if (v._node === "FUNCEXPR" || v._node === "RELABELTYPE" || v._node === "COERCEVIAIO") {
|
|
427
|
+
const args = v._node === "FUNCEXPR" ? argsOf(v).filter(isNode) : isNode(v.arg ?? null) ? [v.arg] : [];
|
|
428
|
+
return args.length >= 1 ? sublinkParamOf(args[0]) : null;
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
/** the argument of Postgres's IMPLICIT time→timetz cast (FUNCEXPR of time_timetz, function 2047, COERCE_IMPLICIT_CAST),
|
|
433
|
+
* or null for any other node: the one test of it (`typesByLocation`, `implicitTimetzCasts`) */
|
|
434
|
+
function implicitTimetzArg(v) {
|
|
435
|
+
// `CASE tm WHEN '12:30-05'::timetz …`: each WHEN compares a placeholder of the CASE's value (CaseTestExpr), which
|
|
436
|
+
// Postgres casts in each: the CASE's own value is the one cast, where every WHEN casts it (`caseCasts`)
|
|
437
|
+
if (v._node === "CASEEXPR" && isNode(v.arg ?? null)) {
|
|
438
|
+
const c = caseCasts(v);
|
|
439
|
+
return c.cast > 0 && c.cast === c.whens ? v.arg : null;
|
|
440
|
+
}
|
|
441
|
+
const a = timetzCastOf(v);
|
|
442
|
+
return a !== null && a._node !== "CASETESTEXPR" ? a : null;
|
|
443
|
+
}
|
|
444
|
+
const timetzCastOf = (v) => {
|
|
445
|
+
if (v === undefined || !isNode(v) || v._node !== "FUNCEXPR" || num(v.funcid ?? null) !== 2047 || num(v.funcformat ?? null) !== 2)
|
|
446
|
+
return null;
|
|
447
|
+
const a = argsOf(v)[0];
|
|
448
|
+
return a !== undefined && isNode(a) ? a : null;
|
|
449
|
+
};
|
|
450
|
+
/** a `CASE x WHEN …`'s WHENs, and how many of them cast its placeholder (x) to a timetz */
|
|
451
|
+
function caseCasts(v) {
|
|
452
|
+
const whens = (Array.isArray(v.args) ? v.args : []).filter(isNode);
|
|
453
|
+
const cast = whens.filter((w) => { const e = w.expr ?? null; const first = isNode(e) ? argsOf(e)[0] : undefined; return timetzCastOf(first)?._node === "CASETESTEXPR"; }).length;
|
|
454
|
+
return { whens: whens.length, cast };
|
|
455
|
+
}
|
|
456
|
+
/** how many implicit time→timetz casts Postgres's analysis holds, a CASE's WHENs that cast its value counted once:
|
|
457
|
+
* each must be written where its argument is (src/duck/rewrite.ts `coerceImplicit` counts what it wrote against this,
|
|
458
|
+
* and refuses the statement when one was not: a CASE whose WHENs cast its value in some and not others among them) */
|
|
459
|
+
export function implicitTimetzCasts(tree) {
|
|
460
|
+
let n = 0;
|
|
461
|
+
const visit = (v) => {
|
|
462
|
+
if (Array.isArray(v)) {
|
|
463
|
+
for (const x of v)
|
|
464
|
+
visit(x);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!isNode(v))
|
|
468
|
+
return;
|
|
469
|
+
const a = timetzCastOf(v);
|
|
470
|
+
if (a !== null && a._node !== "CASETESTEXPR")
|
|
471
|
+
n++;
|
|
472
|
+
if (v._node === "CASEEXPR" && isNode(v.arg ?? null) && caseCasts(v).cast > 0)
|
|
473
|
+
n++;
|
|
474
|
+
for (const [k, x] of Object.entries(v))
|
|
475
|
+
if (k !== "_node")
|
|
476
|
+
visit(x);
|
|
477
|
+
};
|
|
478
|
+
visit(tree);
|
|
479
|
+
return n;
|
|
480
|
+
}
|
|
481
|
+
/** one expression of a query whose range tables are `rtables`, innermost last, as `Typed` says it */
|
|
482
|
+
function typedOf(v, t, rtables) {
|
|
483
|
+
const raw = v._node === "AGGREF" || v._node === "WINDOWFUNC"
|
|
484
|
+
? argsOf(v).map((a) => isNode(a) && a._node === "TARGETENTRY" ? a.expr ?? null : a)
|
|
485
|
+
: argsOf(v);
|
|
486
|
+
const args = raw.filter(isNode).map((a) => exprType(a) ?? { type: 0, typmod: -1 });
|
|
487
|
+
const typed = {
|
|
488
|
+
node: v._node, type: t.type, typmod: t.typmod, args,
|
|
489
|
+
oid: num(v.opno ?? v.funcid ?? v.aggfnoid ?? v.winfnoid ?? null),
|
|
490
|
+
format: num(v.funcformat ?? null),
|
|
491
|
+
collation: collationOf(v),
|
|
492
|
+
inputCollation: num(v.inputcollid ?? null) ?? 0,
|
|
493
|
+
levelsUp: v._node === "VAR" ? num(v.varlevelsup ?? null) : null,
|
|
494
|
+
datum: v._node === "CONST" && v.constisnull !== "true" ? datumBytes(v.constvalue) : null,
|
|
495
|
+
qualifier: v._node === "VAR" ? qualifierOf(v, rtables) : null,
|
|
496
|
+
renamed: v._node === "VAR" ? renamedOf(v, rtables) : null,
|
|
497
|
+
};
|
|
498
|
+
if (constantExpression(v))
|
|
499
|
+
typed.constant = true;
|
|
500
|
+
// a row comparison compares each column under its own collation (RowCompareExpr's inputcollids)
|
|
501
|
+
if (v._node === "ROWCOMPAREEXPR" && Array.isArray(v.inputcollids))
|
|
502
|
+
typed.inputCollations = v.inputcollids.filter((x) => x !== "o").map((x) => typeof x === "string" ? num(x) ?? 0 : 0);
|
|
503
|
+
if (v._node === "ROWCOMPAREEXPR" && Array.isArray(v.largs))
|
|
504
|
+
typed.inputTypes = v.largs.filter(isNode).map((a) => exprType(a)?.type ?? 0);
|
|
505
|
+
// a SubLink's output columns, as the operator reads them (PR 674 class round r3)
|
|
506
|
+
const outputs = v._node === "OPEXPR" ? argsOf(v) : v._node === "ROWCOMPAREEXPR" && Array.isArray(v.rargs) ? v.rargs : [];
|
|
507
|
+
if (outputs.some((a) => sublinkParamOf(a) !== null))
|
|
508
|
+
typed.params = outputs.map(sublinkParamOf);
|
|
509
|
+
if (v._node === "AGGREF" && Array.isArray(v.aggdirectargs))
|
|
510
|
+
typed.directArgs = v.aggdirectargs.filter(isNode).map((a) => exprType(a) ?? { type: 0, typmod: -1 });
|
|
511
|
+
if (v._node === "AGGREF")
|
|
512
|
+
typed.keys = keysOf(v.args, v.aggorder, v.aggdistinct, []);
|
|
513
|
+
// a GROUPING(…): its query level and the GROUP BY entries its arguments read; a window function: its WINDOW clause
|
|
514
|
+
if (v._node === "GROUPINGFUNC")
|
|
515
|
+
typed.grouping = groupingsIn(v)[0];
|
|
516
|
+
if (v._node === "WINDOWFUNC")
|
|
517
|
+
typed.winref = num(v.winref ?? null);
|
|
518
|
+
const text = constText(v);
|
|
519
|
+
if (text !== undefined)
|
|
520
|
+
typed.text = text;
|
|
521
|
+
const textArray = constTextArray(v);
|
|
522
|
+
if (textArray !== undefined)
|
|
523
|
+
typed.textArray = textArray;
|
|
524
|
+
const relation = v._node === "VAR" ? relationOf(v, rtables) : null;
|
|
525
|
+
if (relation !== null)
|
|
526
|
+
typed.relation = relation;
|
|
527
|
+
const column = v._node === "VAR" ? columnOf(v, rtables) : null;
|
|
528
|
+
if (column !== null)
|
|
529
|
+
typed.column = column;
|
|
530
|
+
const fromFunction = v._node === "VAR" ? functionColumnOf(v, rtables) : null;
|
|
531
|
+
if (fromFunction !== null)
|
|
532
|
+
typed.functionColumn = fromFunction;
|
|
533
|
+
return typed;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* THE COLLATION AN EXPRESSION'S RESULT CARRIES, as Postgres's exprCollation reads it: each node keeps it in a field of
|
|
537
|
+
* its own (a COLLATE's `collOid`, a relabel's `resultcollid`, a CASE's `casecollid` …). A SUBLINK's is its subselect's
|
|
538
|
+
* first output's (an EXPR or ARRAY sublink; a test of one carries none). 0: none (a type that is not collatable).
|
|
539
|
+
*/
|
|
540
|
+
export function collationOf(v) {
|
|
541
|
+
if (v._node === "SUBLINK") {
|
|
542
|
+
const sub = v.subselect;
|
|
543
|
+
const tl = isNode(sub ?? null) ? sub.targetList : undefined;
|
|
544
|
+
const first = Array.isArray(tl) ? tl.find((e) => isNode(e) && e.resjunk !== "true") : undefined;
|
|
545
|
+
const kind = num(v.subLinkType);
|
|
546
|
+
return (kind === 4 || kind === 5) && first !== undefined && isNode(first.expr ?? null) ? collationOf(first.expr) : 0;
|
|
547
|
+
}
|
|
548
|
+
return num(v.opcollid ?? v.funccollid ?? v.aggcollid ?? v.wincollid ?? v.varcollid ?? v.constcollid ?? v.collOid ?? v.resultcollid ?? v.casecollid
|
|
549
|
+
?? v.coalescecollid ?? v.minmaxcollid ?? v.array_collid ?? v.paramcollid ?? v.refcollid ?? v.collid ?? v.collation ?? null) ?? 0;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* WHERE A STATEMENT'S ANALYSIS ORDERS A COLLATABLE VALUE, by the collation it orders under: a sort key of any query
|
|
553
|
+
* (its ORDER BY, a window's ORDER BY, an aggregate's ORDER BY or WITHIN GROUP), a GREATEST / LEAST, a row comparison,
|
|
554
|
+
* and — as candidates whose operator or function the caller names from the catalog — an operator's and an aggregate's
|
|
555
|
+
* or window function's input collation (`<` orders, `=` does not; `max` orders, `count` does not), and a plain
|
|
556
|
+
* function's (`calls`: `upper` maps case under it, issue 406; `length` reads none of it).
|
|
557
|
+
*/
|
|
558
|
+
export function textOrdering(tree) {
|
|
559
|
+
const orders = new Set(), operators = new Map(), functions = new Map(), calls = new Map();
|
|
560
|
+
const add = (m, oid, coll) => { if (oid === null || coll === 0)
|
|
561
|
+
return; const s = m.get(oid) ?? new Set(); s.add(coll); m.set(oid, s); };
|
|
562
|
+
const sorted = (clauses, targets) => {
|
|
563
|
+
if (!Array.isArray(clauses) || !Array.isArray(targets))
|
|
564
|
+
return;
|
|
565
|
+
for (const c of clauses) {
|
|
566
|
+
if (!isNode(c))
|
|
567
|
+
continue;
|
|
568
|
+
const ref = num(c.tleSortGroupRef ?? null);
|
|
569
|
+
const t = targets.find((x) => isNode(x) && ref !== null && num(x.ressortgroupref ?? null) === ref);
|
|
570
|
+
const e = t !== undefined && isNode(t.expr ?? null) ? t.expr : null;
|
|
571
|
+
if (e !== null) {
|
|
572
|
+
const coll = collationOf(e);
|
|
573
|
+
if (coll !== 0)
|
|
574
|
+
orders.add(coll);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const visit = (v) => {
|
|
579
|
+
if (Array.isArray(v)) {
|
|
580
|
+
for (const x of v)
|
|
581
|
+
visit(x);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (v === undefined || !isNode(v))
|
|
585
|
+
return;
|
|
586
|
+
if (v._node === "QUERY") {
|
|
587
|
+
sorted(v.sortClause, v.targetList);
|
|
588
|
+
if (Array.isArray(v.windowClause))
|
|
589
|
+
for (const w of v.windowClause)
|
|
590
|
+
if (isNode(w))
|
|
591
|
+
sorted(w.orderClause, v.targetList);
|
|
592
|
+
}
|
|
593
|
+
if (v._node === "AGGREF") {
|
|
594
|
+
sorted(v.aggorder, v.args);
|
|
595
|
+
add(functions, num(v.aggfnoid ?? null), num(v.inputcollid ?? null) ?? 0);
|
|
596
|
+
}
|
|
597
|
+
if (v._node === "WINDOWFUNC")
|
|
598
|
+
add(functions, num(v.winfnoid ?? null), num(v.inputcollid ?? null) ?? 0);
|
|
599
|
+
if (v._node === "OPEXPR")
|
|
600
|
+
add(operators, num(v.opno ?? null), num(v.inputcollid ?? null) ?? 0);
|
|
601
|
+
if (v._node === "FUNCEXPR")
|
|
602
|
+
add(calls, num(v.funcid ?? null), num(v.inputcollid ?? null) ?? 0);
|
|
603
|
+
if (v._node === "MINMAXEXPR") {
|
|
604
|
+
const coll = num(v.inputcollid ?? null) ?? 0;
|
|
605
|
+
if (coll !== 0)
|
|
606
|
+
orders.add(coll);
|
|
607
|
+
}
|
|
608
|
+
if (v._node === "ROWCOMPAREEXPR" && Array.isArray(v.inputcollids))
|
|
609
|
+
for (const x of v.inputcollids) {
|
|
610
|
+
const coll = typeof x === "string" ? num(x) ?? 0 : 0;
|
|
611
|
+
if (coll !== 0)
|
|
612
|
+
orders.add(coll);
|
|
613
|
+
}
|
|
614
|
+
for (const [k, x] of Object.entries(v))
|
|
615
|
+
if (k !== "_node")
|
|
616
|
+
visit(x);
|
|
617
|
+
};
|
|
618
|
+
visit(tree);
|
|
619
|
+
return { orders, operators, functions, calls };
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* WHAT A STATEMENT'S ANALYSIS CALLS: every function, by OID, however it is called (a function's `funcid`, an operator's,
|
|
623
|
+
* NULLIF's or IS DISTINCT FROM's `opfuncid`, an aggregate's `aggfnoid`, a window function's `winfnoid`; 0 where the
|
|
624
|
+
* analysis left one unset), and whether it reads its session by a keyword or a sequence (`current_user`, `user`,
|
|
625
|
+
* `current_catalog`, CURRENT_DATE: a SQLVALUEFUNCTION; nextval's NEXTVALUEEXPR). The caller asks the catalog's pg_proc
|
|
626
|
+
* whether each depends on its arguments alone (`provolatile`), Postgres's own word for it (issue 406).
|
|
627
|
+
*/
|
|
628
|
+
export function functionsCalled(tree) {
|
|
629
|
+
const oids = new Set();
|
|
630
|
+
let session = false;
|
|
631
|
+
const visit = (v) => {
|
|
632
|
+
if (Array.isArray(v)) {
|
|
633
|
+
for (const x of v)
|
|
634
|
+
visit(x);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (v === undefined || !isNode(v))
|
|
638
|
+
return;
|
|
639
|
+
if (v._node === "SQLVALUEFUNCTION" || v._node === "NEXTVALUEEXPR" || v._node === "CURRENTOFEXPR")
|
|
640
|
+
session = true;
|
|
641
|
+
for (const k of ["funcid", "opfuncid", "aggfnoid", "winfnoid"])
|
|
642
|
+
if (k in v)
|
|
643
|
+
oids.add(num(v[k] ?? null) ?? 0);
|
|
245
644
|
for (const [k, x] of Object.entries(v))
|
|
246
645
|
if (k !== "_node")
|
|
247
646
|
visit(x);
|
|
248
647
|
};
|
|
249
648
|
visit(tree);
|
|
649
|
+
return { oids, session };
|
|
650
|
+
}
|
|
651
|
+
/** EVERY COLLATION A STATEMENT'S ANALYSIS NAMES (an expression's, an operator's input, a row comparison's per column, a
|
|
652
|
+
* FROM item's columns'), by OID: what src/duck/collation.ts resolves before the rewrite reads any */
|
|
653
|
+
export function collationOids(tree) {
|
|
654
|
+
const out = new Set();
|
|
655
|
+
const field = /(collid|collids|collOid|^collation|^colcollations)$/;
|
|
656
|
+
const visit = (v, key) => {
|
|
657
|
+
if (Array.isArray(v)) {
|
|
658
|
+
if (field.test(key)) {
|
|
659
|
+
for (const x of v)
|
|
660
|
+
if (typeof x === "string" && x !== "o") {
|
|
661
|
+
const n = num(x);
|
|
662
|
+
if (n !== null)
|
|
663
|
+
out.add(n);
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
for (const x of v)
|
|
668
|
+
visit(x, "");
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (typeof v === "string") {
|
|
672
|
+
if (field.test(key)) {
|
|
673
|
+
const n = num(v);
|
|
674
|
+
if (n !== null)
|
|
675
|
+
out.add(n);
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (v === undefined || !isNode(v))
|
|
680
|
+
return;
|
|
681
|
+
for (const [k, x] of Object.entries(v))
|
|
682
|
+
if (k !== "_node")
|
|
683
|
+
visit(x, k);
|
|
684
|
+
};
|
|
685
|
+
visit(tree, "");
|
|
686
|
+
out.delete(0);
|
|
250
687
|
return out;
|
|
251
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Each SELECT's outputs, found by any offset its targets hold. THE ORDER OF A `*` IS ITS QUERY'S `targetList`, NEVER THE
|
|
691
|
+
* ORDER ITS VARS ARE MET IN THE TREE: under a GROUP BY (Postgres 17+) a key's output is a VAR of the RTE_GROUP entry at
|
|
692
|
+
* no offset, and the column it groups is in that entry's `groupexprs`, which comes first and in the GROUP BY's order. So
|
|
693
|
+
* the offset of a star says nothing of which of its columns is which; a set operation's own outputs are left out, as
|
|
694
|
+
* they carry its first SELECT's offsets.
|
|
695
|
+
*/
|
|
696
|
+
export function outputsByLocation(tree) {
|
|
697
|
+
return analysedQueries(tree).byOffset;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Each SELECT's outputs, by any offset its targets hold (`outputsByLocation`), and those of each SELECT whose outputs hold
|
|
701
|
+
* none: `TABLE t`, whose `*` the grammar writes at no offset, so Postgres's expansion of it is at none either. Such a
|
|
702
|
+
* one is known by the FROM items it reads, by the names its range table gives them (`eref`), in order, and by the
|
|
703
|
+
* offsets its own LIMIT and OFFSET hold (`limits`).
|
|
704
|
+
*/
|
|
705
|
+
export function analysedQueries(tree) {
|
|
706
|
+
const found = new Map();
|
|
707
|
+
const unplaced = [];
|
|
708
|
+
const visit = (v) => {
|
|
709
|
+
if (Array.isArray(v)) {
|
|
710
|
+
for (const x of v)
|
|
711
|
+
visit(x);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (!isNode(v))
|
|
715
|
+
return;
|
|
716
|
+
if (v._node === "QUERY" && Array.isArray(v.targetList) && !isNode(v.setOperations ?? null)) {
|
|
717
|
+
const rtable = Array.isArray(v.rtable) ? v.rtable.filter(isNode) : [];
|
|
718
|
+
const entries = v.targetList.filter(isNode).filter((t) => t.resjunk !== "true");
|
|
719
|
+
const exprs = entries.map((t) => ungrouped(t.expr ?? null, rtable));
|
|
720
|
+
const widths = new Map();
|
|
721
|
+
for (const rte of rtable) {
|
|
722
|
+
const eref = rte.eref ?? null;
|
|
723
|
+
if (isNode(eref) && typeof eref.aliasname === "string" && !widths.has(eref.aliasname))
|
|
724
|
+
widths.set(eref.aliasname, strings(eref.colnames).filter((c) => c !== "").length);
|
|
725
|
+
}
|
|
726
|
+
const joins = rtable.filter((rte) => num(rte.rtekind ?? null) === RTE_JOIN).map((rte) => {
|
|
727
|
+
const eref = rte.eref ?? null;
|
|
728
|
+
return isNode(eref) ? strings(eref.colnames).slice(0, num(rte.joinmergedcols ?? null) ?? 0) : [];
|
|
729
|
+
});
|
|
730
|
+
const outputs = {
|
|
731
|
+
columns: exprs.map((e) => { const t = isNode(e) && e._node === "VAR" ? exprType(e) : null; return t === null ? null : typedOf(e, t, [rtable]); }),
|
|
732
|
+
widths,
|
|
733
|
+
joins,
|
|
734
|
+
keys: keysOf(v.targetList, v.sortClause, v.distinctClause, rtable, v.windowClause),
|
|
735
|
+
};
|
|
736
|
+
let placed = false;
|
|
737
|
+
const offsets = (x) => {
|
|
738
|
+
if (Array.isArray(x)) {
|
|
739
|
+
x.forEach(offsets);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
// (a subquery in a target is a query of its own, which registers its own offsets)
|
|
743
|
+
if (!isNode(x) || x._node === "QUERY")
|
|
744
|
+
return;
|
|
745
|
+
const loc = num(x.location);
|
|
746
|
+
if (loc !== null && loc >= 0) {
|
|
747
|
+
placed = true;
|
|
748
|
+
if (!found.has(loc))
|
|
749
|
+
found.set(loc, outputs);
|
|
750
|
+
}
|
|
751
|
+
const inner = ungrouped(x, rtable);
|
|
752
|
+
if (inner !== x) {
|
|
753
|
+
offsets(inner);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
for (const [k, y] of Object.entries(x))
|
|
757
|
+
if (k !== "_node")
|
|
758
|
+
offsets(y);
|
|
759
|
+
};
|
|
760
|
+
offsets(exprs);
|
|
761
|
+
if (!placed) {
|
|
762
|
+
const from = rtable.filter((rte) => num(rte.rtekind ?? null) !== RTE_JOIN).map((rte) => { const eref = rte.eref ?? null; return isNode(eref) && typeof eref.aliasname === "string" ? eref.aliasname : ""; });
|
|
763
|
+
unplaced.push({ from, limits: limitOffsets(v), outputs });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
for (const [k, x] of Object.entries(v))
|
|
767
|
+
if (k !== "_node")
|
|
768
|
+
visit(x);
|
|
769
|
+
};
|
|
770
|
+
visit(tree);
|
|
771
|
+
return { byOffset: found, unplaced };
|
|
772
|
+
}
|
|
773
|
+
/** the offsets a query's own LIMIT and OFFSET hold (a count of `FETCH FIRST ROW` the grammar writes at none) */
|
|
774
|
+
function limitOffsets(q) {
|
|
775
|
+
const held = (x) => {
|
|
776
|
+
if (Array.isArray(x))
|
|
777
|
+
return x.flatMap(held);
|
|
778
|
+
if (x === undefined || !isNode(x) || x._node === "QUERY")
|
|
779
|
+
return [];
|
|
780
|
+
const loc = num(x.location);
|
|
781
|
+
return [...(loc !== null && loc >= 0 ? [loc] : []), ...Object.entries(x).flatMap(([k, y]) => k === "_node" ? [] : held(y))];
|
|
782
|
+
};
|
|
783
|
+
return [...held(q.limitCount), ...held(q.limitOffset)];
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* EACH SET OPERATION'S OWN OUTPUTS, AS POSTGRES RESOLVED THEM: a column's type, typmod and collation are the set
|
|
787
|
+
* operation's (`SELECT <citext> … UNION ALL SELECT <text> …` is text, its first SELECT's is citext), never its first
|
|
788
|
+
* SELECT's. Each is known by the SELECTs it combines, in order: the offsets their outputs hold (a SELECT that is a set
|
|
789
|
+
* operation of its own, with an ORDER BY or a LIMIT, is one of them, by its first SELECT's; a `TABLE t` by none, as
|
|
790
|
+
* its outputs hold none), and the offsets its own LIMIT and OFFSET hold. Each column is typed as no table's column:
|
|
791
|
+
* what is known of it is the set operation's alone.
|
|
792
|
+
*/
|
|
793
|
+
export function setOperationOutputs(tree) {
|
|
794
|
+
const found = [];
|
|
795
|
+
const held = (x) => {
|
|
796
|
+
if (Array.isArray(x))
|
|
797
|
+
return x.flatMap(held);
|
|
798
|
+
// (a subquery in an output is a query of its own)
|
|
799
|
+
if (x === undefined || !isNode(x) || x._node === "QUERY")
|
|
800
|
+
return [];
|
|
801
|
+
const loc = num(x.location);
|
|
802
|
+
return [...(loc !== null && loc >= 0 ? [loc] : []), ...Object.entries(x).flatMap(([k, y]) => k === "_node" ? [] : held(y))];
|
|
803
|
+
};
|
|
804
|
+
const visit = (v) => {
|
|
805
|
+
if (Array.isArray(v)) {
|
|
806
|
+
for (const x of v)
|
|
807
|
+
visit(x);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (!isNode(v))
|
|
811
|
+
return;
|
|
812
|
+
if (v._node === "QUERY" && isNode(v.setOperations ?? null) && Array.isArray(v.targetList)) {
|
|
813
|
+
const rtable = Array.isArray(v.rtable) ? v.rtable : [];
|
|
814
|
+
const arms = [];
|
|
815
|
+
// each arm's own output types, before the set operation coerced them to its columns' (null: not known)
|
|
816
|
+
const armTypes = [];
|
|
817
|
+
const leaves = (s) => {
|
|
818
|
+
if (s === undefined || !isNode(s))
|
|
819
|
+
return;
|
|
820
|
+
if (s._node === "SETOPERATIONSTMT") {
|
|
821
|
+
leaves(s.larg);
|
|
822
|
+
leaves(s.rarg);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const rte = s._node === "RANGETBLREF" ? rtable[(num(s.rtindex ?? null) ?? 0) - 1] : undefined;
|
|
826
|
+
const q = isNode(rte ?? null) ? rte.subquery ?? null : null;
|
|
827
|
+
if (!isNode(q)) {
|
|
828
|
+
arms.push([]);
|
|
829
|
+
armTypes.push(null);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
armTypes.push(Array.isArray(q.targetList) ? q.targetList.filter((t) => isNode(t) && t.resjunk !== "true").map((t) => { const e = t.expr ?? null; return isNode(e) ? exprType(e)?.type ?? 0 : 0; }) : null);
|
|
833
|
+
const own = Array.isArray(q.targetList) ? held(q.targetList.filter((t) => isNode(t) && t.resjunk !== "true").map((t) => t.expr ?? null)) : [];
|
|
834
|
+
// (a VALUES's outputs read its rows at no offset of their own: it is known by its rows')
|
|
835
|
+
const rows = own.length > 0 || !Array.isArray(q.rtable) ? [] : q.rtable.filter(isNode).flatMap((r) => held(r.values_lists));
|
|
836
|
+
arms.push([...own, ...rows]);
|
|
837
|
+
};
|
|
838
|
+
leaves(v.setOperations);
|
|
839
|
+
const columns = v.targetList.filter(isNode).filter((t) => t.resjunk !== "true").map((t) => {
|
|
840
|
+
const e = t.expr ?? null;
|
|
841
|
+
const type = isNode(e) ? exprType(e) : null;
|
|
842
|
+
if (!isNode(e) || type === null)
|
|
843
|
+
return null;
|
|
844
|
+
const { relation: _r, column: _c, functionColumn: _f, ...own } = typedOf(e, type, [rtable.filter(isNode)]);
|
|
845
|
+
return { ...own, qualifier: null, renamed: null };
|
|
846
|
+
});
|
|
847
|
+
found.push({ arms, limits: limitOffsets(v), columns, armTypes });
|
|
848
|
+
}
|
|
849
|
+
for (const [k, x] of Object.entries(v))
|
|
850
|
+
if (k !== "_node")
|
|
851
|
+
visit(x);
|
|
852
|
+
};
|
|
853
|
+
visit(tree);
|
|
854
|
+
return found;
|
|
855
|
+
}
|
|
856
|
+
/** an output as what it reads: a VAR of the query's RTE_GROUP is the expression it groups */
|
|
857
|
+
function ungrouped(v, rtable) {
|
|
858
|
+
if (!isNode(v) || v._node !== "VAR")
|
|
859
|
+
return v;
|
|
860
|
+
const rte = rtable[(num(v.varno ?? null) ?? 0) - 1];
|
|
861
|
+
const exprs = rte !== undefined && num(rte.rtekind ?? null) === RTE_GROUP ? rte.groupexprs : undefined;
|
|
862
|
+
return Array.isArray(exprs) ? exprs[(num(v.varattno ?? null) ?? 0) - 1] ?? v : v;
|
|
863
|
+
}
|
|
252
864
|
const RTE_JOIN = 2;
|
|
865
|
+
const RTE_GROUP = 9;
|
|
253
866
|
const RTE_RELATION = 0;
|
|
867
|
+
/** the FROM items whose name in the analysis (`unnamed_join`, `*VALUES*`, `unnamed_subquery`) no statement can say:
|
|
868
|
+
* RTE_SUBQUERY, RTE_JOIN, RTE_VALUES */
|
|
869
|
+
const UNNAMED = new Set([1, RTE_JOIN, 5]);
|
|
254
870
|
/** Typed.relation: the table a VAR's FROM item is, by the catalog's OID, and the column's number in it */
|
|
255
871
|
function relationOf(v, rtables) {
|
|
256
872
|
const up = num(v.varlevelsup ?? null) ?? 0, varno = num(v.varno ?? null), attno = num(v.varattno ?? null);
|
|
@@ -258,6 +874,21 @@ function relationOf(v, rtables) {
|
|
|
258
874
|
const relid = rte === undefined ? null : num(rte.relid ?? null);
|
|
259
875
|
return rte === undefined || attno === null || attno < 1 || num(rte.rtekind ?? null) !== RTE_RELATION || relid === null || relid === 0 ? null : { relid, attno };
|
|
260
876
|
}
|
|
877
|
+
/** Typed.column */
|
|
878
|
+
function columnOf(v, rtables) {
|
|
879
|
+
const up = num(v.varlevelsup ?? null) ?? 0, varno = num(v.varnosyn ?? null), attno = num(v.varattnosyn ?? null);
|
|
880
|
+
const rte = varno === null ? undefined : rtables[rtables.length - 1 - up]?.[varno - 1];
|
|
881
|
+
if (rte === undefined || attno === null || attno < 1 || !isNode(rte.eref ?? null))
|
|
882
|
+
return null;
|
|
883
|
+
const eref = rte.eref;
|
|
884
|
+
const columns = strings(eref.colnames), name = columns[attno - 1];
|
|
885
|
+
if (name === undefined || typeof eref.aliasname !== "string" || /^\*SELECT\* \d+$/.test(eref.aliasname))
|
|
886
|
+
return null;
|
|
887
|
+
const kind = num(rte.rtekind ?? null) ?? -1;
|
|
888
|
+
const named = isNode(rte.alias ?? null);
|
|
889
|
+
return { owner: UNNAMED.has(kind) && !named ? null : eref.aliasname, name,
|
|
890
|
+
...(kind === RTE_JOIN && named && columns.filter((c) => c === name).length > 1 ? { twin: true } : {}) };
|
|
891
|
+
}
|
|
261
892
|
const RTE_FUNCTION = 3;
|
|
262
893
|
/** Typed.functionColumn */
|
|
263
894
|
function functionColumnOf(v, rtables) {
|
|
@@ -367,6 +998,126 @@ export function joinsUsing(tree, types) {
|
|
|
367
998
|
};
|
|
368
999
|
return visit(tree);
|
|
369
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* The analysed expression of each output column, in order (resjunk ones left out): what computes its value, every node
|
|
1003
|
+
* with the type Postgres gave it, FOLLOWED wherever the analysis says a value comes from. A VAR that reads a FROM
|
|
1004
|
+
* subquery (a generated table's `order_by:` shell among them), a CTE, a set operation's arms (each arm's column), a join's
|
|
1005
|
+
* alias column or a grouped key carries what it reads as `followed`, itself followed; a sublink's subselect is followed
|
|
1006
|
+
* as a query of its own, its VARs read against the queries around it. A recursive CTE's reading of itself is not
|
|
1007
|
+
* followed again. Null for no query.
|
|
1008
|
+
*
|
|
1009
|
+
* LINEAR IN THE ANALYSED TREE, NEVER IN ITS EXPANSION (PR 202 round 14): a VAR's `followed` is the ONE followed node of
|
|
1010
|
+
* what it reads, by reference, shared by every VAR that reads it (`Follow`), so the result is a graph whose shared nodes
|
|
1011
|
+
* a reader visits once (src/duck/generated-plan.ts memoises by node). A copy per VAR grew 3^depth for a statement whose
|
|
1012
|
+
* levels each read the level below three times: 880 ms at depth 12, about 70 s at 16. The generated planner alone asks
|
|
1013
|
+
* for this (src/duck/plan.ts `PlanOptions.outputs`); a wire statement never pays for it.
|
|
1014
|
+
*/
|
|
1015
|
+
export function outputExpressions(tree) {
|
|
1016
|
+
if (!isNode(tree) || tree._node !== "QUERY" || !Array.isArray(tree.targetList))
|
|
1017
|
+
return null;
|
|
1018
|
+
const top = tree, list = tree.targetList, walk = new Follow();
|
|
1019
|
+
// a set operation's own column is a VAR of its leftmost arm's: it reads every arm's, and its location is the leftmost
|
|
1020
|
+
// arm's expression's, no column reference of its own (-1)
|
|
1021
|
+
const own = (e) => isNode(top.setOperations ?? null)
|
|
1022
|
+
? { ...e.expr, location: "-1", followed: walk.outputOf(top, num(e.resno ?? null) ?? 0, []) } : walk.followed(e.expr, [top]);
|
|
1023
|
+
return list.filter((e) => isNode(e) && e.resjunk !== "true").map((e) => isNode(e.expr ?? null) ? own(e) : null);
|
|
1024
|
+
}
|
|
1025
|
+
/** the followed walk of one analysed tree (`outputExpressions`): each analysed node followed ONCE and each query's output
|
|
1026
|
+
* column found once, memoised by the analysed node, every VAR that reads one sharing the result. A node's stack (the
|
|
1027
|
+
* queries around it) is where it stands in the tree, so one result serves every path to it; `open` is the queries being
|
|
1028
|
+
* followed, so a recursive CTE's reading of itself reads nothing */
|
|
1029
|
+
class Follow {
|
|
1030
|
+
nodes = new Map();
|
|
1031
|
+
columns = new Map();
|
|
1032
|
+
open = new Set();
|
|
1033
|
+
/** `v` with every VAR it holds followed; `stack` the queries around it, its own last */
|
|
1034
|
+
followed(v, stack) {
|
|
1035
|
+
if (Array.isArray(v))
|
|
1036
|
+
return v.map((x) => this.followed(x, stack));
|
|
1037
|
+
if (!isNode(v))
|
|
1038
|
+
return v;
|
|
1039
|
+
const done = this.nodes.get(v);
|
|
1040
|
+
if (done !== undefined)
|
|
1041
|
+
return done;
|
|
1042
|
+
const result = this.follow(v, stack);
|
|
1043
|
+
this.nodes.set(v, result);
|
|
1044
|
+
return result;
|
|
1045
|
+
}
|
|
1046
|
+
follow(v, stack) {
|
|
1047
|
+
const inner = v._node === "QUERY" ? [...stack, v] : stack;
|
|
1048
|
+
const out = Object.fromEntries(Object.entries(v).map(([k, x]) => [k, k === "_node" ? x : this.followed(x, inner)]));
|
|
1049
|
+
if (v._node !== "VAR")
|
|
1050
|
+
return out;
|
|
1051
|
+
const up = num(v.varlevelsup ?? null) ?? 0, attno = num(v.varattno ?? null);
|
|
1052
|
+
const base = stack.slice(0, stack.length - up), q = base[base.length - 1];
|
|
1053
|
+
if (q === undefined || attno === null || attno < 1 || !Array.isArray(q.rtable))
|
|
1054
|
+
return out;
|
|
1055
|
+
const rte = q.rtable[(num(v.varno ?? null) ?? 0) - 1];
|
|
1056
|
+
if (rte === undefined || !isNode(rte))
|
|
1057
|
+
return out;
|
|
1058
|
+
const kind = num(rte.rtekind ?? null);
|
|
1059
|
+
let reads = [];
|
|
1060
|
+
if (kind === RTE_SUBQUERY && isNode(rte.subquery ?? null))
|
|
1061
|
+
reads = this.outputOf(rte.subquery, attno, base);
|
|
1062
|
+
else if (kind === RTE_CTE) {
|
|
1063
|
+
// the CTE of the query `ctelevelsup` above, read with that query's own outer queries around it
|
|
1064
|
+
const levels = num(rte.ctelevelsup ?? null) ?? 0, owner = base[base.length - 1 - levels];
|
|
1065
|
+
const cte = owner !== undefined && Array.isArray(owner.cteList) ? owner.cteList.find((c) => isNode(c) && c.ctename === rte.ctename) : undefined;
|
|
1066
|
+
if (cte !== undefined && isNode(cte) && isNode(cte.ctequery ?? null))
|
|
1067
|
+
reads = this.outputOf(cte.ctequery, attno, base.slice(0, base.length - levels));
|
|
1068
|
+
}
|
|
1069
|
+
else if (kind === RTE_JOIN || kind === RTE_GROUP) {
|
|
1070
|
+
const list = kind === RTE_JOIN ? rte.joinaliasvars : rte.groupexprs;
|
|
1071
|
+
const e = Array.isArray(list) ? list[attno - 1] : undefined;
|
|
1072
|
+
if (e !== undefined && isNode(e))
|
|
1073
|
+
reads = [this.followed(e, base)];
|
|
1074
|
+
}
|
|
1075
|
+
return reads.length === 0 ? out : { ...out, followed: reads };
|
|
1076
|
+
}
|
|
1077
|
+
/** what output column `attno` of query `q` computes, followed: one expression, or a set operation's, one per arm */
|
|
1078
|
+
outputOf(q, attno, outer) {
|
|
1079
|
+
if (this.open.has(q))
|
|
1080
|
+
return [];
|
|
1081
|
+
const known = this.columns.get(q)?.get(attno);
|
|
1082
|
+
if (known !== undefined)
|
|
1083
|
+
return known;
|
|
1084
|
+
this.open.add(q);
|
|
1085
|
+
let found;
|
|
1086
|
+
try {
|
|
1087
|
+
found = this.outputFound(q, attno, [...outer, q]);
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
this.open.delete(q);
|
|
1091
|
+
}
|
|
1092
|
+
const byColumn = this.columns.get(q) ?? new Map();
|
|
1093
|
+
this.columns.set(q, byColumn.set(attno, found));
|
|
1094
|
+
return found;
|
|
1095
|
+
}
|
|
1096
|
+
outputFound(q, attno, stack) {
|
|
1097
|
+
if (isNode(q.setOperations ?? null)) {
|
|
1098
|
+
const leaves = [];
|
|
1099
|
+
const walk = (s) => {
|
|
1100
|
+
if (s === undefined || !isNode(s))
|
|
1101
|
+
return;
|
|
1102
|
+
if (s._node === "RANGETBLREF")
|
|
1103
|
+
leaves.push(num(s.rtindex ?? null) ?? 0);
|
|
1104
|
+
else {
|
|
1105
|
+
walk(s.larg);
|
|
1106
|
+
walk(s.rarg);
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
walk(q.setOperations);
|
|
1110
|
+
return leaves.flatMap((i) => {
|
|
1111
|
+
const rte = Array.isArray(q.rtable) ? q.rtable[i - 1] : undefined;
|
|
1112
|
+
return rte !== undefined && isNode(rte) && isNode(rte.subquery ?? null) ? this.outputOf(rte.subquery, attno, stack) : [];
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const entry = Array.isArray(q.targetList) ? q.targetList.find((t) => isNode(t) && num(t.resno ?? null) === attno) : undefined;
|
|
1116
|
+
return entry !== undefined && isNode(entry) && isNode(entry.expr ?? null) ? [this.followed(entry.expr, stack)] : [];
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const RTE_SUBQUERY = 1;
|
|
1120
|
+
const RTE_CTE = 6;
|
|
370
1121
|
/** The analysed query's output columns, in order (resjunk ones, which a sort may add, left out): their types. */
|
|
371
1122
|
export function outputTypes(tree) {
|
|
372
1123
|
if (!isNode(tree) || tree._node !== "QUERY" || !Array.isArray(tree.targetList))
|