@mason-data/cli 0.0.0-stage → 0.6.2
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/LICENSE +202 -0
- package/README.md +209 -2
- package/dist/src/api/error.js +18 -0
- package/dist/src/api/postgres/catalog-worker.js +92 -0
- package/dist/src/api/postgres/catalog.js +442 -0
- package/dist/src/api/postgres/encode.js +541 -0
- package/dist/src/api/postgres/explain.js +143 -0
- package/dist/src/api/postgres/listener.js +60 -0
- package/dist/src/api/postgres/parser-worker.js +58 -0
- package/dist/src/api/postgres/parser.js +121 -0
- package/dist/src/api/postgres/plain.js +33 -0
- package/dist/src/api/postgres/protocol.js +404 -0
- package/dist/src/api/postgres/schema.js +54 -0
- package/dist/src/api/postgres/server.js +1670 -0
- package/dist/src/api/postgres/translate.js +248 -0
- package/dist/src/api/request-log.js +50 -0
- package/dist/src/cdc/pg-wire.js +482 -0
- package/dist/src/cdc/pgoutput.js +107 -0
- package/dist/src/cdc/poll.js +67 -0
- package/dist/src/cdc/source.js +65 -0
- package/dist/src/cli/background.js +174 -0
- package/dist/src/cli/command.js +172 -0
- package/dist/src/cli/commands.js +1145 -0
- package/dist/src/cli/control.js +36 -0
- package/dist/src/cli/dev.js +576 -0
- package/dist/src/cli/doctor.js +257 -0
- package/dist/src/cli/guides.js +134 -0
- package/dist/src/cli/init.js +326 -0
- package/dist/src/cli/machine.js +157 -0
- package/dist/src/cli/main.js +83 -0
- package/dist/src/cli/pg-tls.js +100 -0
- package/dist/src/cli/pg-url.js +52 -0
- package/dist/src/cli/pin.js +36 -0
- package/dist/src/cli/ports.js +118 -0
- package/dist/src/cli/preflight.js +338 -0
- package/dist/src/cli/project.js +127 -0
- package/dist/src/cli/readiness.js +76 -0
- package/dist/src/cli/registry.js +217 -0
- package/dist/src/cli/replication-checks.js +120 -0
- package/dist/src/cli/replication-mode.js +148 -0
- package/dist/src/cli/run-server.js +388 -0
- package/dist/src/cli/runs.js +120 -0
- package/dist/src/cli/security.js +86 -0
- package/dist/src/cli/self-signed-cert.js +64 -0
- package/dist/src/cli/serve.js +296 -0
- package/dist/src/cli/sources.js +562 -0
- package/dist/src/cli/state.js +58 -0
- package/dist/src/cli/style.js +187 -0
- package/dist/src/cli/supervise.js +248 -0
- package/dist/src/cli/sync.js +94 -0
- package/dist/src/cli/ui.js +107 -0
- package/dist/src/cli/user.js +229 -0
- package/dist/src/cli/version.js +23 -0
- package/dist/src/duck/analyze.js +113 -0
- package/dist/src/duck/cancel.js +49 -0
- package/dist/src/duck/executor-worker.js +263 -0
- package/dist/src/duck/executor.js +526 -0
- package/dist/src/duck/explain.js +119 -0
- package/dist/src/duck/functions.js +764 -0
- package/dist/src/duck/gaps.js +89 -0
- package/dist/src/duck/jsonb.js +186 -0
- package/dist/src/duck/kept-route.js +219 -0
- package/dist/src/duck/kept.js +797 -0
- package/dist/src/duck/late.js +221 -0
- package/dist/src/duck/libc-order.js +190 -0
- package/dist/src/duck/pg-eval-worker.js +34 -0
- package/dist/src/duck/pg-eval.js +59 -0
- package/dist/src/duck/pgtree.js +338 -0
- package/dist/src/duck/plan.js +246 -0
- package/dist/src/duck/publication.js +207 -0
- package/dist/src/duck/requests.js +209 -0
- package/dist/src/duck/resolved.js +248 -0
- package/dist/src/duck/rewrite.js +4001 -0
- package/dist/src/duck/store.js +831 -0
- package/dist/src/duck/stream.js +518 -0
- package/dist/src/duck/sync.js +2864 -0
- package/dist/src/duck/to-char.js +482 -0
- package/dist/src/duck/token.js +31 -0
- package/dist/src/duck/types.js +427 -0
- package/dist/src/duck/users.js +231 -0
- package/dist/src/duck/verified.js +995 -0
- package/dist/src/duck/views.js +150 -0
- package/dist/src/lib/database-url.js +74 -0
- package/dist/src/lib/did-you-mean.js +43 -0
- package/dist/src/lib/dotenv.js +72 -0
- package/dist/src/lib/exit-when-written.js +20 -0
- package/dist/src/lib/float-equal.js +69 -0
- package/dist/src/lib/http-json.js +22 -0
- package/dist/src/lib/log.js +161 -0
- package/dist/src/lib/pg-numeric.js +489 -0
- package/dist/src/lib/port.js +22 -0
- package/dist/src/lib/project-file.js +49 -0
- package/dist/src/lib/project-files.js +43 -0
- package/dist/src/lib/project-version.js +76 -0
- package/dist/src/lib/runnable.js +14 -0
- package/dist/src/lib/shell-word.js +4 -0
- package/dist/src/lib/timezone.js +236 -0
- package/dist/src/lib/zone-names.js +114 -0
- package/dist/src/web/app.css +65 -0
- package/dist/src/web/app.js +336 -0
- package/dist/src/web/index.html +39 -0
- package/package.json +22 -4
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POSTGRES'S OWN ANALYSIS OF A STATEMENT, READ (docs/DESIGN.md "Postgres decides, DuckDB computes"). The
|
|
3
|
+
* catalog Postgres (src/api/postgres/catalog.ts) holds every copied table under its source types; with
|
|
4
|
+
* `debug_print_parse` on it logs the ANALYSED query tree of a statement it parses — every expression with the type
|
|
5
|
+
* Postgres resolved for it (the operator it chose, the implicit casts it inserted, an aggregate's result type) and the
|
|
6
|
+
* offset into the text it came from. This module reads that tree (Postgres's `outfuncs.c` node format) and indexes it
|
|
7
|
+
* by offset, so the rewrite (src/duck/rewrite.ts) knows, for each node of the RAW parse tree it rewrites, what
|
|
8
|
+
* Postgres made of it: `/` over a `sum(bigint)` is a numeric division, `round(x)` of a double precision is `rint`, a
|
|
9
|
+
* text sort key sorts by the database's collation.
|
|
10
|
+
*
|
|
11
|
+
* The format: `{NODE :field value …}`; a value is a node, a list `( … )` (of nodes or of values; `(b …)` a bitmapset,
|
|
12
|
+
* `(o …)` OIDs, `(i …)` ints), `<>` for NULL, a quoted string, a bare token (outToken backslash-escapes the
|
|
13
|
+
* characters that would end one), or a datum `N [ b b … ]` (a Const's value).
|
|
14
|
+
*/
|
|
15
|
+
/** Parses one node-format text (outfuncs.c's nodeToString), or throws with the offset where it went wrong. */
|
|
16
|
+
export function parseTree(text) {
|
|
17
|
+
let i = 0;
|
|
18
|
+
const ws = () => { while (i < text.length && /\s/.test(text[i]))
|
|
19
|
+
i++; };
|
|
20
|
+
const fail = (what) => { throw new Error(`the catalog Postgres's parse tree: ${what} at offset ${i}: ${JSON.stringify(text.slice(i, i + 40))}`); };
|
|
21
|
+
const bare = () => {
|
|
22
|
+
let s = "";
|
|
23
|
+
while (i < text.length && !/[\s(){}]/.test(text[i])) {
|
|
24
|
+
if (text[i] === "\\" && i + 1 < text.length) {
|
|
25
|
+
s += text[i + 1];
|
|
26
|
+
i += 2;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
s += text[i++];
|
|
30
|
+
}
|
|
31
|
+
return s;
|
|
32
|
+
};
|
|
33
|
+
const value = () => {
|
|
34
|
+
ws();
|
|
35
|
+
const c = text[i];
|
|
36
|
+
if (c === undefined)
|
|
37
|
+
return fail("the text ends inside a value");
|
|
38
|
+
if (c === "{")
|
|
39
|
+
return node();
|
|
40
|
+
if (c === "(") {
|
|
41
|
+
i++;
|
|
42
|
+
const out = [];
|
|
43
|
+
for (;;) {
|
|
44
|
+
ws();
|
|
45
|
+
if (text[i] === ")") {
|
|
46
|
+
i++;
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
if (i >= text.length)
|
|
50
|
+
return fail("a list is not closed");
|
|
51
|
+
out.push(value());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (c === "<" && text[i + 1] === ">") {
|
|
55
|
+
i += 2;
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (c === '"') {
|
|
59
|
+
i++;
|
|
60
|
+
let s = "";
|
|
61
|
+
while (i < text.length && text[i] !== '"') {
|
|
62
|
+
if (text[i] === "\\" && i + 1 < text.length) {
|
|
63
|
+
s += text[i + 1];
|
|
64
|
+
i += 2;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
s += text[i++];
|
|
68
|
+
}
|
|
69
|
+
i++;
|
|
70
|
+
return s;
|
|
71
|
+
}
|
|
72
|
+
const tok = bare();
|
|
73
|
+
// a datum: its length, then its bytes in brackets
|
|
74
|
+
const save = i;
|
|
75
|
+
ws();
|
|
76
|
+
if (text[i] === "[") {
|
|
77
|
+
const end = text.indexOf("]", i);
|
|
78
|
+
if (end < 0)
|
|
79
|
+
return fail("a datum is not closed");
|
|
80
|
+
const datum = `${tok} ${text.slice(i, end + 1)}`;
|
|
81
|
+
i = end + 1;
|
|
82
|
+
return datum;
|
|
83
|
+
}
|
|
84
|
+
i = save;
|
|
85
|
+
return tok;
|
|
86
|
+
};
|
|
87
|
+
const node = () => {
|
|
88
|
+
i++; // {
|
|
89
|
+
ws();
|
|
90
|
+
const name = bare();
|
|
91
|
+
const out = { _node: name };
|
|
92
|
+
for (;;) {
|
|
93
|
+
ws();
|
|
94
|
+
if (text[i] === "}") {
|
|
95
|
+
i++;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
if (text[i] !== ":")
|
|
99
|
+
return fail(`a field of ${name} must start with ':'`);
|
|
100
|
+
i++;
|
|
101
|
+
const field = bare();
|
|
102
|
+
out[field] = value();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const v = value();
|
|
106
|
+
ws();
|
|
107
|
+
if (i < text.length)
|
|
108
|
+
fail("text after the tree");
|
|
109
|
+
return v;
|
|
110
|
+
}
|
|
111
|
+
const isNode = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
112
|
+
const num = (v) => typeof v === "string" && /^-?\d+$/.test(v) ? Number(v) : null;
|
|
113
|
+
/** a `constvalue`'s bytes: `<length> [ b0 b1 … ]`, each a signed byte */
|
|
114
|
+
export function datumBytes(v) {
|
|
115
|
+
if (typeof v !== "string")
|
|
116
|
+
return null;
|
|
117
|
+
const m = /^\s*\d+\s*\[([-\d\s]*)\]\s*$/.exec(v);
|
|
118
|
+
return m ? m[1].trim().split(/\s+/).filter((x) => x !== "").map((x) => Number(x) & 255) : null;
|
|
119
|
+
}
|
|
120
|
+
const BOOL = 16;
|
|
121
|
+
/** The type an expression node yields: exprType (nodeFuncs.c), for the nodes a SELECT's expressions are made of. */
|
|
122
|
+
export function exprType(n) {
|
|
123
|
+
const t = (f, m) => {
|
|
124
|
+
const v = num(n[f]);
|
|
125
|
+
return v === null ? null : { type: v, typmod: m === undefined ? -1 : num(n[m]) ?? -1 };
|
|
126
|
+
};
|
|
127
|
+
switch (n._node) {
|
|
128
|
+
case "VAR": return t("vartype", "vartypmod");
|
|
129
|
+
case "CONST": return t("consttype", "consttypmod");
|
|
130
|
+
case "PARAM": return t("paramtype", "paramtypmod");
|
|
131
|
+
case "AGGREF": return t("aggtype");
|
|
132
|
+
case "WINDOWFUNC": return t("wintype");
|
|
133
|
+
case "FUNCEXPR": {
|
|
134
|
+
const r = t("funcresulttype");
|
|
135
|
+
// a length coercion (numeric(p, s), varchar(n)) carries its typmod as its second argument's constant
|
|
136
|
+
if (r && num(n.funcformat) !== 0 && Array.isArray(n.args) && n.args.length >= 2 && isNode(n.args[1]) && n.args[1]._node === "CONST") {
|
|
137
|
+
const datum = n.args[1].constvalue;
|
|
138
|
+
// the bytes as debug_print_parse wraps them: any whitespace, a line break included (`4 [\n2 0 0 0 ]`)
|
|
139
|
+
const m = typeof datum === "string" ? /\[\s*(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s*\]/.exec(datum) : null;
|
|
140
|
+
if (m)
|
|
141
|
+
r.typmod = (Number(m[1]) & 255) | ((Number(m[2]) & 255) << 8) | ((Number(m[3]) & 255) << 16) | ((Number(m[4]) & 255) << 24);
|
|
142
|
+
}
|
|
143
|
+
return r;
|
|
144
|
+
}
|
|
145
|
+
case "OPEXPR":
|
|
146
|
+
case "DISTINCTEXPR":
|
|
147
|
+
case "NULLIFEXPR": return n._node === "DISTINCTEXPR" ? { type: BOOL, typmod: -1 } : t("opresulttype");
|
|
148
|
+
case "SCALARARRAYOPEXPR":
|
|
149
|
+
case "BOOLEXPR":
|
|
150
|
+
case "NULLTEST":
|
|
151
|
+
case "BOOLEANTEST": return { type: BOOL, typmod: -1 };
|
|
152
|
+
case "RELABELTYPE":
|
|
153
|
+
case "COERCEVIAIO":
|
|
154
|
+
case "ARRAYCOERCEEXPR":
|
|
155
|
+
case "COERCETODOMAIN": return t("resulttype", "resulttypmod");
|
|
156
|
+
case "CASEEXPR": return t("casetype");
|
|
157
|
+
case "COALESCEEXPR": return t("coalescetype");
|
|
158
|
+
case "MINMAXEXPR": return t("minmaxtype");
|
|
159
|
+
case "SQLVALUEFUNCTION": return t("type", "typmod");
|
|
160
|
+
case "FIELDSELECT": return t("resulttype", "resulttypmod");
|
|
161
|
+
case "SUBSCRIPTINGREF": return t("refrestype", "reftypmod");
|
|
162
|
+
case "ARRAYEXPR": return t("array_typeid");
|
|
163
|
+
case "ROWEXPR": return t("row_typeid");
|
|
164
|
+
case "COLLATEEXPR": return Array.isArray(n.arg) ? null : isNode(n.arg ?? null) ? exprType(n.arg) : null;
|
|
165
|
+
case "SUBLINK": {
|
|
166
|
+
// an EXPR or ARRAY sublink's type is its subselect's first output column's
|
|
167
|
+
const sub = n.subselect;
|
|
168
|
+
if (!isNode(sub ?? null))
|
|
169
|
+
return null;
|
|
170
|
+
const tl = sub.targetList;
|
|
171
|
+
const first = Array.isArray(tl) ? tl.find((e) => isNode(e) && e.resjunk !== "true") : undefined;
|
|
172
|
+
const st = first && isNode(first.expr ?? null) ? exprType(first.expr) : null;
|
|
173
|
+
const kind = num(n.subLinkType);
|
|
174
|
+
return kind === 4 /* EXPR */ ? st : kind === 5 /* ARRAY */ ? null : { type: BOOL, typmod: -1 };
|
|
175
|
+
}
|
|
176
|
+
default: return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const argsOf = (n) => {
|
|
180
|
+
if (Array.isArray(n.args))
|
|
181
|
+
return n.args;
|
|
182
|
+
if (isNode(n.arg ?? null))
|
|
183
|
+
return [n.arg];
|
|
184
|
+
return [];
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Every typed expression of the analysed tree, by the offset it came from — outermost first where several share one
|
|
188
|
+
* (a cast and the coercion inside it). An implicit cast Postgres inserted has no offset of its own (-1) and is listed
|
|
189
|
+
* only as its parent's argument type. AGGREF's arguments are its TARGETENTRY exprs.
|
|
190
|
+
*/
|
|
191
|
+
export function typesByLocation(tree) {
|
|
192
|
+
const out = new Map();
|
|
193
|
+
// the range tables of the queries around the node being visited, innermost last
|
|
194
|
+
const rtables = [];
|
|
195
|
+
const visit = (v) => {
|
|
196
|
+
if (Array.isArray(v)) {
|
|
197
|
+
for (const x of v)
|
|
198
|
+
visit(x);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!isNode(v))
|
|
202
|
+
return;
|
|
203
|
+
if (v._node === "QUERY") {
|
|
204
|
+
rtables.push(Array.isArray(v.rtable) ? v.rtable.filter(isNode) : []);
|
|
205
|
+
try {
|
|
206
|
+
for (const [k, x] of Object.entries(v))
|
|
207
|
+
if (k !== "_node")
|
|
208
|
+
visit(x);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
rtables.pop();
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const loc = num(v.location);
|
|
216
|
+
const t = exprType(v);
|
|
217
|
+
if (loc !== null && loc >= 0 && t !== null) {
|
|
218
|
+
const raw = v._node === "AGGREF" || v._node === "WINDOWFUNC"
|
|
219
|
+
? argsOf(v).map((a) => isNode(a) && a._node === "TARGETENTRY" ? a.expr ?? null : a)
|
|
220
|
+
: argsOf(v);
|
|
221
|
+
const args = raw.filter(isNode).map((a) => exprType(a) ?? { type: 0, typmod: -1 });
|
|
222
|
+
const typed = {
|
|
223
|
+
node: v._node, type: t.type, typmod: t.typmod, args,
|
|
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 list = out.get(loc);
|
|
237
|
+
if (list)
|
|
238
|
+
list.push(typed);
|
|
239
|
+
else
|
|
240
|
+
out.set(loc, [typed]);
|
|
241
|
+
}
|
|
242
|
+
for (const [k, x] of Object.entries(v))
|
|
243
|
+
if (k !== "_node")
|
|
244
|
+
visit(x);
|
|
245
|
+
};
|
|
246
|
+
visit(tree);
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
const RTE_JOIN = 2;
|
|
250
|
+
const RTE_RELATION = 0;
|
|
251
|
+
/** Typed.relation: the table a VAR's FROM item is, by the catalog's OID, and the column's number in it */
|
|
252
|
+
function relationOf(v, rtables) {
|
|
253
|
+
const up = num(v.varlevelsup ?? null) ?? 0, varno = num(v.varno ?? null), attno = num(v.varattno ?? null);
|
|
254
|
+
const rte = varno === null ? undefined : rtables[rtables.length - 1 - up]?.[varno - 1];
|
|
255
|
+
const relid = rte === undefined ? null : num(rte.relid ?? null);
|
|
256
|
+
return rte === undefined || attno === null || attno < 1 || num(rte.rtekind ?? null) !== RTE_RELATION || relid === null || relid === 0 ? null : { relid, attno };
|
|
257
|
+
}
|
|
258
|
+
const strings = (v) => Array.isArray(v) ? v.map((x) => typeof x === "string" ? x : "") : [];
|
|
259
|
+
/** Typed.qualifier: the name of a VAR's FROM item when another FROM item of its query has the column in another case,
|
|
260
|
+
* or when the VAR is an outer query's (a correlated subquery's bare `i4`, which DuckDB binds to the subquery's own
|
|
261
|
+
* output name `i4` first and refuses, 42703) */
|
|
262
|
+
function qualifierOf(v, rtables) {
|
|
263
|
+
const up = num(v.varlevelsup ?? null) ?? 0, varno = num(v.varno ?? null), attno = num(v.varattno ?? null);
|
|
264
|
+
const rtable = rtables[rtables.length - 1 - up];
|
|
265
|
+
if (rtable === undefined || varno === null || attno === null || attno < 1)
|
|
266
|
+
return null;
|
|
267
|
+
const rte = rtable[varno - 1];
|
|
268
|
+
const eref = rte !== undefined && isNode(rte.eref ?? null) ? rte.eref : null;
|
|
269
|
+
if (rte === undefined || eref === null || num(rte.rtekind ?? null) === RTE_JOIN)
|
|
270
|
+
return null;
|
|
271
|
+
const alias = typeof eref.aliasname === "string" ? eref.aliasname : null;
|
|
272
|
+
const column = strings(eref.colnames)[attno - 1];
|
|
273
|
+
// a name the query cannot spell (an unnamed subquery, a VALUES list) cannot qualify anything
|
|
274
|
+
if (alias === null || column === undefined || alias === "unnamed_subquery" || alias === "*VALUES*")
|
|
275
|
+
return null;
|
|
276
|
+
const lower = column.toLowerCase();
|
|
277
|
+
// a FROM item of its own query, or of a query inside it the VAR is read from (a correlated subquery's, which DuckDB's
|
|
278
|
+
// binder looks in first)
|
|
279
|
+
const clash = rtables.slice(rtables.length - 1 - up).flat().some((other) => other !== rte && num(other.rtekind ?? null) !== RTE_JOIN
|
|
280
|
+
&& isNode(other.eref ?? null) && strings(other.eref.colnames).some((c) => c !== column && c.toLowerCase() === lower));
|
|
281
|
+
// an outer column is qualified unless a query inside has a FROM item of the same name (the name would be that one)
|
|
282
|
+
const outer = up > 0 && !rtables.slice(rtables.length - up).flat().some((other) => isNode(other.eref ?? null)
|
|
283
|
+
&& String(other.eref.aliasname ?? "").toLowerCase() === alias.toLowerCase());
|
|
284
|
+
return clash || outer ? alias : null;
|
|
285
|
+
}
|
|
286
|
+
/** a FROM item's columns with each one that has a twin in another case renamed `<name>__mason_<n>`; null when none has */
|
|
287
|
+
function twinNames(rte) {
|
|
288
|
+
if (num(rte.rtekind ?? null) === RTE_JOIN || num(rte.rtekind ?? null) === 0 || !isNode(rte.eref ?? null))
|
|
289
|
+
return null;
|
|
290
|
+
const cols = strings(rte.eref.colnames);
|
|
291
|
+
const lower = cols.map((c) => c.toLowerCase());
|
|
292
|
+
const twin = (i) => lower.some((l, j) => j !== i && l === lower[i] && cols[j] !== cols[i]);
|
|
293
|
+
return cols.some((_, i) => twin(i)) ? cols.map((c, i) => twin(i) ? `${c}__mason_${i + 1}` : c) : null;
|
|
294
|
+
}
|
|
295
|
+
/** Typed.renamed */
|
|
296
|
+
function renamedOf(v, rtables) {
|
|
297
|
+
const up = num(v.varlevelsup ?? null) ?? 0, varno = num(v.varno ?? null), attno = num(v.varattno ?? null);
|
|
298
|
+
const rte = varno === null ? undefined : rtables[rtables.length - 1 - up]?.[varno - 1];
|
|
299
|
+
if (rte === undefined || attno === null || attno < 1)
|
|
300
|
+
return null;
|
|
301
|
+
const names = twinNames(rte);
|
|
302
|
+
const own = isNode(rte.eref ?? null) ? strings(rte.eref.colnames)[attno - 1] : undefined;
|
|
303
|
+
return names !== null && names[attno - 1] !== own ? names[attno - 1] ?? null : null;
|
|
304
|
+
}
|
|
305
|
+
/** Every subquery or CTE read in FROM whose columns have twins in another case (`"T"` beside `t`), by the name the
|
|
306
|
+
* query calls it: its columns as DuckDB is to name them (twinNames), every reference renamed alike (Typed.renamed) */
|
|
307
|
+
export function caseTwins(tree) {
|
|
308
|
+
const out = new Map();
|
|
309
|
+
const visit = (v) => {
|
|
310
|
+
if (Array.isArray(v)) {
|
|
311
|
+
for (const x of v)
|
|
312
|
+
visit(x);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (!isNode(v))
|
|
316
|
+
return;
|
|
317
|
+
if (v._node === "RANGETBLENTRY" && isNode(v.eref ?? null)) {
|
|
318
|
+
const names = twinNames(v);
|
|
319
|
+
const alias = v.eref.aliasname;
|
|
320
|
+
if (names !== null && typeof alias === "string")
|
|
321
|
+
out.set(alias, names);
|
|
322
|
+
}
|
|
323
|
+
for (const [k, x] of Object.entries(v))
|
|
324
|
+
if (k !== "_node")
|
|
325
|
+
visit(x);
|
|
326
|
+
};
|
|
327
|
+
visit(tree);
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
/** The analysed query's output columns, in order (resjunk ones, which a sort may add, left out): their types. */
|
|
331
|
+
export function outputTypes(tree) {
|
|
332
|
+
if (!isNode(tree) || tree._node !== "QUERY" || !Array.isArray(tree.targetList))
|
|
333
|
+
return null;
|
|
334
|
+
return tree.targetList.filter((e) => isNode(e) && e.resjunk !== "true").map((e) => {
|
|
335
|
+
const t = isNode(e.expr ?? null) ? exprType(e.expr) : null;
|
|
336
|
+
return { name: typeof e.resname === "string" ? e.resname : null, type: t?.type ?? 0, typmod: t?.typmod ?? -1 };
|
|
337
|
+
});
|
|
338
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE STATEMENT'S DUCKDB PLAN (docs/DESIGN.md "Postgres decides, DuckDB computes"): Postgres's analysis of
|
|
3
|
+
* it (src/duck/analyze.ts) — its refusal, or its result's columns and its parameters' types — and the DuckDB SQL that
|
|
4
|
+
* computes the same answer (src/duck/rewrite.ts), printed (pgsql-deparser) and read back to be sure it is the tree the
|
|
5
|
+
* rewrite meant. The Postgres listener asks for a plan at Parse (and for each statement of a simple query), and keeps it
|
|
6
|
+
* per text until the schema changes.
|
|
7
|
+
*/
|
|
8
|
+
import { deparseSync, QuoteUtils } from "pgsql-deparser";
|
|
9
|
+
import { StatementError } from "../api/postgres/server.js";
|
|
10
|
+
import { parseSql } from "../api/postgres/parser.js";
|
|
11
|
+
import { analyze, catalogRows, readParameters } from "./analyze.js";
|
|
12
|
+
import { describeKeys, nameUserKeys, resolvedKeys } from "./resolved.js";
|
|
13
|
+
import { gapMark } from "./gaps.js";
|
|
14
|
+
import { expandViews, MAX_VIEW_DEPTH, viewRefused } from "./views.js";
|
|
15
|
+
import { selectListAfterLimit } from "./late.js";
|
|
16
|
+
import { pgTyplen } from "./types.js";
|
|
17
|
+
import { datumText, masonCalls, rewrite, treeDifference, Unrewritable } from "./rewrite.js";
|
|
18
|
+
import { exposes, hideKept, readKey, searchSchemas } from "./kept-route.js";
|
|
19
|
+
const JSON_OID = 114, JSONB_OID = 3802;
|
|
20
|
+
/** what the rewrite asks of the stored JSON paths (src/duck/kept-route.ts KeptLookup): the copied table and column of
|
|
21
|
+
* each json or jsonb column the statement reads from a table (Typed.relation), by the catalog's OIDs; null when it
|
|
22
|
+
* reads none */
|
|
23
|
+
async function keptLookup(catalog, types, kept) {
|
|
24
|
+
const wanted = new Map();
|
|
25
|
+
for (const t of [...types.values()].flat())
|
|
26
|
+
if (t.relation !== undefined && (t.type === JSON_OID || t.type === JSONB_OID))
|
|
27
|
+
wanted.set(`${t.relation.relid}.${t.relation.attno}`, t.relation);
|
|
28
|
+
if (wanted.size === 0)
|
|
29
|
+
return null;
|
|
30
|
+
const pairs = [...wanted.values()].map((r) => `(${Math.trunc(r.relid)}, ${Math.trunc(r.attno)})`).join(", ");
|
|
31
|
+
const rows = await catalogRows(catalog, `SELECT c.oid::text, a.attnum::text, n.nspname, c.relname, a.attname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace ` +
|
|
32
|
+
`JOIN pg_attribute a ON a.attrelid = c.oid WHERE (c.oid::int8, a.attnum::int8) IN (${pairs}) AND c.relkind = 'r' AND NOT a.attisdropped`);
|
|
33
|
+
const at = new Map(rows.map(([relid, attno, schema, table, column]) => [`${relid}.${attno}`, { source: `${schema}.${table}`, column: column }]));
|
|
34
|
+
return { at: (relid, attno) => at.get(`${relid}.${attno}`) ?? null, ready: (read) => kept.ready(read) };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* DuckDB's keywords that Postgres's are not: a column named `at` (DuckDB's AT TIME ZONE) or `qualify` is an identifier
|
|
38
|
+
* the deparser writes bare, as Postgres reads it, and DuckDB refuses as a syntax error. The deparser's one quoting
|
|
39
|
+
* function (`QuoteUtils.quoteIdentifier`, its every bare identifier) also quotes these, read from DuckDB itself
|
|
40
|
+
* (`duckdb_keywords()`, every category but the unreserved) by the store at the listener's start.
|
|
41
|
+
*/
|
|
42
|
+
const duckKeywords = new Set();
|
|
43
|
+
const pgQuote = QuoteUtils.quoteIdentifier.bind(QuoteUtils);
|
|
44
|
+
export function quoteDuckKeywords(words) {
|
|
45
|
+
for (const w of words)
|
|
46
|
+
duckKeywords.add(w.toLowerCase());
|
|
47
|
+
QuoteUtils.quoteIdentifier = (ident) => {
|
|
48
|
+
const q = pgQuote(ident);
|
|
49
|
+
return q === ident && duckKeywords.has(ident) ? `"${ident}"` : q;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** a search_path as SET writes it: each schema quoted (`"$user"` kept) */
|
|
53
|
+
const pathSql = (searchPath) => searchPath.split(",").map((s) => s.trim()).filter((s) => s !== "")
|
|
54
|
+
.map((s) => s.startsWith('"') ? s : `"${s.replaceAll('"', '""')}"`).join(", ") || "public";
|
|
55
|
+
/**
|
|
56
|
+
* `verified`: what the oracle has held to Postgres (src/duck/verified.ts). A statement using anything else is REFUSED,
|
|
57
|
+
* by name — never answered by a rewrite nobody checked. null only for the oracle itself, which records what it verifies.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* A statement's parameter values as DuckDB must read them: what Postgres's input function made of each (readParameters,
|
|
61
|
+
* src/duck/analyze.ts), written back in a form DuckDB reads to the same value. A value Postgres refuses is refused with
|
|
62
|
+
* its error (`'1.5'` for a smallint: 22P02).
|
|
63
|
+
*/
|
|
64
|
+
export async function duckParameters(catalog, oids, values, timeZone) {
|
|
65
|
+
const read = await readParameters(catalog, oids, values, timeZone);
|
|
66
|
+
if ("refused" in read) {
|
|
67
|
+
const f = read.refused;
|
|
68
|
+
throw new StatementError({ code: f.C ?? "22P02", message: f.M ?? "invalid parameter value", ...(f.D ? { detail: f.D } : {}), ...(f.H ? { hint: f.H } : {}) }, "");
|
|
69
|
+
}
|
|
70
|
+
return values.map((v, i) => {
|
|
71
|
+
const r = read.values[i];
|
|
72
|
+
if (v === null || r === undefined || r === null)
|
|
73
|
+
return v;
|
|
74
|
+
if (/^-?infinity$/.test(r.text))
|
|
75
|
+
return r.text;
|
|
76
|
+
const le = (n, bytes) => Array.from({ length: bytes }, (_, k) => Number((BigInt.asUintN(bytes * 8, n) >> BigInt(8 * k)) & 255n));
|
|
77
|
+
if (r.oid === 1082)
|
|
78
|
+
return datumText(1082, le(BigInt(r.text), 4));
|
|
79
|
+
if (r.oid === 1114 || r.oid === 1184)
|
|
80
|
+
return datumText(r.oid, le(BigInt(r.text), 8));
|
|
81
|
+
if (r.oid === 1186) {
|
|
82
|
+
const [m, d, us] = r.text.split(" ");
|
|
83
|
+
return `${m} months ${d} days ${us} microseconds`;
|
|
84
|
+
}
|
|
85
|
+
return r.text;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* THE WAY OUT OF `relation "x" does not exist`, from the sync's copy state (`Sync.state`, the one reader: a
|
|
90
|
+
* table added and still copying was told "mason add x copies it", which it already was). A table the catalog does not
|
|
91
|
+
* hold is one of: added and waiting its turn, added and copying (how far), added and failed (why, and when it is tried
|
|
92
|
+
* again), copied a moment ago (the catalog is rebuilt as each copy commits), or not added — the one `mason add` away.
|
|
93
|
+
* `relation` is the name as Postgres wrote it: qualified, or bare, which matches an added table of that name (public's first).
|
|
94
|
+
*/
|
|
95
|
+
export function missingRelationHint(relation, copies) {
|
|
96
|
+
const matches = copies.filter((c) => c.source === relation || (!relation.includes(".") && c.source.endsWith(`.${relation}`)))
|
|
97
|
+
.sort((a, b) => Number(b.source.startsWith("public.")) - Number(a.source.startsWith("public.")));
|
|
98
|
+
const c = matches[0];
|
|
99
|
+
const served = "it is served once its copy commits; mason wait waits for it (mason status shows how far it is)";
|
|
100
|
+
if (c === undefined)
|
|
101
|
+
return `a table is in Mason once it is added: mason add ${relation} copies it (mason tables lists the database's)`;
|
|
102
|
+
switch (c.phase) {
|
|
103
|
+
case "waiting": return `${c.source} is added and waits its turn to be copied: ${served}`;
|
|
104
|
+
case "copying": {
|
|
105
|
+
// the share is of the heap's PAGES read (`CopyProgressView.share`: exact, so it never moves backwards), the count
|
|
106
|
+
// the rows the committed chunks and the ones reading have in; a copy can outrun the source's estimate (Postgres's
|
|
107
|
+
// statistics lag the table) and then says so, never a share that fell to 0 (the same class as mason
|
|
108
|
+
// wait's bar: a share derived from an estimate must never move backwards)
|
|
109
|
+
const p = c.progress;
|
|
110
|
+
const so = c.rowsSoFar === null || c.rowsSoFar === undefined ? null : c.rowsSoFar + (p?.rowsReading ?? 0), of = c.rowsEstimate ?? null;
|
|
111
|
+
const share = p === undefined ? "" : `${Math.floor(p.share * 100)}% of its pages read, `;
|
|
112
|
+
const how = so === null ? `${share}its rows are being read`
|
|
113
|
+
: of === null || of <= 0 ? `${share}about ${so} rows so far`
|
|
114
|
+
: so > of ? `${share}${so} rows so far, past the ~${of} estimated`
|
|
115
|
+
: `${share}about ${so} of ~${of} rows so far${p === undefined ? `, ${Math.min(99, Math.floor((so / of) * 100))}%` : ""}`;
|
|
116
|
+
return `${c.source} is added and being copied (${how}): ${served}`;
|
|
117
|
+
}
|
|
118
|
+
case "failed": return `${c.source} is added, but its copy failed: ${c.error ?? "no reason recorded"}; mason logs says more`;
|
|
119
|
+
case "copied": return `${c.source} was just copied and joins the catalog as it is rebuilt: run the statement again in a moment (if it stays missing, mason logs says why)`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function planDuck(catalog, text, stmt, base, paramOids, searchPath, timeZone, collation, verified, userTypes = new Map(), copies = () => [], depth = 0, kept = null) {
|
|
123
|
+
const started = performance.now();
|
|
124
|
+
const analysis = await analyze(catalog, text, paramOids, pathSql(searchPath), timeZone);
|
|
125
|
+
if ("refused" in analysis) {
|
|
126
|
+
const f = analysis.refused;
|
|
127
|
+
// a table the catalog does not hold: where its copy is, or the `mason add` away
|
|
128
|
+
const missing = f.C === "42P01" ? /^relation "([^"]+)" does not exist$/.exec(f.M ?? "")?.[1] : undefined;
|
|
129
|
+
const hint = missing !== undefined ? missingRelationHint(missing, copies()) : f.H;
|
|
130
|
+
throw new StatementError({ code: f.C ?? "XX000", message: f.M ?? "Postgres refused the statement", ...(f.D ? { detail: f.D } : {}), ...(hint ? { hint } : {}),
|
|
131
|
+
...(f.P ? { position: Number(f.P) } : {}) }, text);
|
|
132
|
+
}
|
|
133
|
+
// A VIEW is answered as Postgres answers it, by its definition in its place (src/duck/views.ts): the statement so
|
|
134
|
+
// written is planned from the start, its parameters typed as this one's were. Its errors are the expanded text's, so
|
|
135
|
+
// they carry no position into this one
|
|
136
|
+
const expanded = await expandViews(catalog, stmt, analysis, pathSql(searchPath));
|
|
137
|
+
if (expanded !== null) {
|
|
138
|
+
if (depth >= MAX_VIEW_DEPTH)
|
|
139
|
+
throw viewRefused(`Mason cannot yet answer views read through more than ${MAX_VIEW_DEPTH} other views`, "read the tables the views read");
|
|
140
|
+
try {
|
|
141
|
+
return await planDuck(catalog, expanded.text, expanded.stmt, 0, analysis.params, searchPath, timeZone, collation, verified, userTypes, copies, depth + 1, kept);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (!(e instanceof StatementError))
|
|
145
|
+
throw e;
|
|
146
|
+
const { position: _p, ...fields } = e.fields;
|
|
147
|
+
throw new StatementError(fields, text);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// the catalog's own OIDs for the types an extension made (citext): the rewrite knows a few by name, and the wire sends
|
|
151
|
+
// the SOURCE's OID for them (`userTypes`, from the copy), never the catalog's
|
|
152
|
+
let resolved = resolvedKeys(analysis.tree);
|
|
153
|
+
const catalogTypes = new Map();
|
|
154
|
+
const enumLabels = new Map(), enumArrays = new Map();
|
|
155
|
+
const enums = { labels: enumLabels, arrays: enumArrays };
|
|
156
|
+
const fields = analysis.fields === null ? null : analysis.fields.map((f) => ({ ...f }));
|
|
157
|
+
// a table's row type (a whole row, `SELECT t FROM t`): its columns, which the rewrite writes the row from, and the
|
|
158
|
+
// name the wire maps to the source's OID (`rowtype:<schema>.<table>`)
|
|
159
|
+
const rowTypes = new Map();
|
|
160
|
+
const used = new Set([...analysis.types.values()].flat().map((t) => t.type).filter((o) => o >= 16384));
|
|
161
|
+
if ([...resolved].some((k) => /\bu\d+/.test(k)) || fields?.some((f) => f.oid >= 16384) || used.size > 0) {
|
|
162
|
+
for (const [oid, name] of await catalogRows(catalog, "SELECT oid::text, typname FROM pg_type WHERE oid >= 16384 AND typtype IN ('b', 'd', 'e')"))
|
|
163
|
+
catalogTypes.set(Number(oid), name);
|
|
164
|
+
// an enum's labels in their declared order, by its OID and its array type's: DuckDB is handed each value as its ENUM
|
|
165
|
+
for (const [oid, array, label] of await catalogRows(catalog, "SELECT t.oid::text, t.typarray::text, e.enumlabel FROM pg_type t JOIN pg_enum e ON e.enumtypid = t.oid ORDER BY t.oid, e.enumsortorder")) {
|
|
166
|
+
const labels = enumLabels.get(Number(oid)) ?? [];
|
|
167
|
+
labels.push(label);
|
|
168
|
+
enumLabels.set(Number(oid), labels);
|
|
169
|
+
if (Number(array) > 0)
|
|
170
|
+
enumArrays.set(Number(array), Number(oid));
|
|
171
|
+
}
|
|
172
|
+
for (const [oid, name] of await catalogRows(catalog, "SELECT t.oid::text, 'rowtype:' || n.nspname || '.' || t.typname FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.oid >= 16384 AND t.typtype = 'c'")) {
|
|
173
|
+
catalogTypes.set(Number(oid), name);
|
|
174
|
+
if (!used.has(Number(oid)))
|
|
175
|
+
continue;
|
|
176
|
+
const columns = await catalogRows(catalog, `SELECT a.attname, a.atttypid::text, a.atttypmod::text FROM pg_attribute a JOIN pg_type t ON t.typrelid = a.attrelid WHERE t.oid = ${Number(oid)} AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`);
|
|
177
|
+
rowTypes.set(Number(oid), columns.map(([n, type, typmod]) => ({ name: n, type: Number(type), typmod: Number(typmod) })));
|
|
178
|
+
}
|
|
179
|
+
resolved = await nameUserKeys((sql) => catalogRows(catalog, sql), resolved);
|
|
180
|
+
for (const f of fields ?? []) {
|
|
181
|
+
const source = f.oid >= 16384 ? userTypes.get(catalogTypes.get(f.oid) ?? "") : undefined;
|
|
182
|
+
if (source !== undefined)
|
|
183
|
+
f.oid = source;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let tree, dropLast, exact, clock, jsonReads;
|
|
187
|
+
// a table's row the source has otherwise (copied in part: no source OID for it) is not the source's row
|
|
188
|
+
const sourceRows = new Map([...rowTypes].filter(([oid]) => userTypes.has(catalogTypes.get(oid) ?? "")));
|
|
189
|
+
// the stored JSON paths the statement's JSON reads may be answered from (src/duck/kept-route.ts)
|
|
190
|
+
const lookup = kept === null ? null : await keptLookup(catalog, analysis.types, kept);
|
|
191
|
+
const analysed = performance.now();
|
|
192
|
+
const rewritten = (l = lookup) => rewrite(stmt, base, analysis, collation, catalogTypes, sourceRows, timeZone, enums, l);
|
|
193
|
+
try {
|
|
194
|
+
({ tree, dropLast, exact, clock, jsonReads } = rewritten());
|
|
195
|
+
// the oracle's (`eager`): every read stored before the plan is made, and the plan made again over them
|
|
196
|
+
if (kept?.keepNow != null && lookup !== null && jsonReads.some((r) => kept.ready(r) === null)) {
|
|
197
|
+
await kept.keepNow(jsonReads);
|
|
198
|
+
({ tree, dropLast, exact, clock, jsonReads } = rewritten());
|
|
199
|
+
}
|
|
200
|
+
// a statement that lists a copy's columns without naming them reads none from a stored column (hideKept): made
|
|
201
|
+
// again with every read its call, the reads still recorded
|
|
202
|
+
if (lookup !== null && exposes(tree))
|
|
203
|
+
({ tree, dropLast, exact, clock, jsonReads } = rewritten({ at: (relid, attno) => lookup.at(relid, attno), ready: () => null }));
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
// a gap says its id (docs/SQL-SUPPORT.md); Postgres's own refusal is its SQLSTATE, and no gap
|
|
207
|
+
if (e instanceof Unrewritable) {
|
|
208
|
+
throw new StatementError(typeof e.gap === "string"
|
|
209
|
+
? { code: "0A000", message: e.message, hint: e.hint, detail: `${gapMark(e.gap)} (docs/SQL-SUPPORT.md)` }
|
|
210
|
+
: { code: e.gap.postgres, message: e.message, hint: e.hint }, text);
|
|
211
|
+
}
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
// a statement that lists a copy's columns without naming them reads its visible ones (src/duck/kept-route.ts); before
|
|
215
|
+
// the late select list, which then reads the columns this leaves
|
|
216
|
+
if (kept !== null)
|
|
217
|
+
hideKept(tree, { visible: kept.visible(), searchPath: searchSchemas(searchPath) });
|
|
218
|
+
// a sorted, limited select list is computed over the rows the limit keeps (src/duck/late.ts)
|
|
219
|
+
tree = selectListAfterLimit(tree, analysis.tree) ?? tree;
|
|
220
|
+
// ONLY WHAT THE ORACLE VERIFIED: after the rewrite, so a construct the rewrite refuses by name (with its own way out)
|
|
221
|
+
// says so first
|
|
222
|
+
const keys = [...resolved].sort();
|
|
223
|
+
const unverified = verified === null ? [] : keys.filter((k) => !verified.has(k));
|
|
224
|
+
if (unverified.length > 0) {
|
|
225
|
+
const names = await describeKeys((sql) => catalogRows(catalog, sql), unverified);
|
|
226
|
+
const listed = unverified.map((k) => names.get(k) ?? k);
|
|
227
|
+
throw new StatementError({ code: "0A000",
|
|
228
|
+
message: `Mason cannot yet answer this statement on DuckDB exactly as Postgres would: it uses ${listed.slice(0, 5).join("; ")}${listed.length > 5 ? ` and ${listed.length - 5} more` : ""}, which Mason has not verified`,
|
|
229
|
+
hint: "write the statement without it (docs/SQL-SUPPORT.md lists what is verified), or report it: a statement Mason has not verified is refused rather than answered differently",
|
|
230
|
+
detail: `${gapMark("unverified")}: ${unverified.join(", ")}` }, text);
|
|
231
|
+
}
|
|
232
|
+
// DuckDB's COLUMNS(t.*) (decidedByKey's star) is an expression only unquoted; the deparser quotes the keyword. No
|
|
233
|
+
// statement Postgres accepted calls a function "columns" (Postgres has none), and the read-back below checks the text
|
|
234
|
+
const sql = deparseSync(tree, { pretty: false }).replaceAll('any_value("columns"(', "any_value(COLUMNS(");
|
|
235
|
+
// THE PRINTED TEXT MUST READ BACK AS THE TREE: a printing gap (the deparser drops or respells something) would run
|
|
236
|
+
// another statement than the one Postgres analysed — refused instead, by name
|
|
237
|
+
const back = (await parseSql(sql)).stmts?.[0]?.stmt;
|
|
238
|
+
const difference = back === undefined ? "nothing" : treeDifference(back, tree);
|
|
239
|
+
if (difference !== null) {
|
|
240
|
+
throw new StatementError({ code: "0A000", message: "Mason cannot run this statement on DuckDB exactly: its rewritten form does not read back as itself (a Mason gap: report it, with the statement)",
|
|
241
|
+
detail: `read back: ${difference}; DuckDB SQL: ${sql}` }, text);
|
|
242
|
+
}
|
|
243
|
+
const planned = { text: sql, functions: masonCalls(tree) };
|
|
244
|
+
return { sql: planned, params: analysis.params, fields: fields?.map((f) => ({ ...f, size: pgTyplen(f.oid) })) ?? null, dropLast, ...(exact.length > 0 ? { exact } : {}), resolved: keys,
|
|
245
|
+
...(Object.keys(clock).length > 0 ? { clock } : {}), ...(jsonReads.length > 0 ? { jsonReads: jsonReads.map(readKey) } : {}), timing: { analysisMs: analysed - started, rewriteMs: performance.now() - analysed } };
|
|
246
|
+
}
|