@mason-data/cli 0.6.2 → 0.8.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 +147 -69
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog.js +258 -14
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/server.js +1290 -223
- package/dist/src/api/postgres/translate.js +81 -9
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/pg-wire.js +144 -38
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +55 -8
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +5 -3
- package/dist/src/cli/commands.js +843 -272
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/demo.js +158 -0
- package/dist/src/cli/dev.js +458 -125
- package/dist/src/cli/doctor.js +25 -12
- package/dist/src/cli/guides.js +103 -31
- package/dist/src/cli/init.js +159 -58
- package/dist/src/cli/machine.js +18 -12
- package/dist/src/cli/main.js +5 -5
- package/dist/src/cli/mcp.js +735 -0
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/pg-tls.js +13 -3
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +13 -12
- package/dist/src/cli/preflight.js +24 -14
- package/dist/src/cli/project.js +48 -7
- package/dist/src/cli/readiness.js +38 -4
- package/dist/src/cli/registry.js +57 -35
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +584 -0
- package/dist/src/cli/run-server.js +498 -133
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +10 -8
- package/dist/src/cli/serve.js +147 -40
- package/dist/src/cli/share-run.js +286 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +168 -0
- package/dist/src/cli/sources.js +290 -39
- package/dist/src/cli/supervise.js +12 -1
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui.js +9 -6
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user.js +496 -40
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +240 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/events.js +134 -0
- package/dist/src/duck/executor-worker.js +25 -11
- package/dist/src/duck/executor.js +107 -53
- package/dist/src/duck/explain.js +158 -29
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +73 -19
- package/dist/src/duck/gaps.js +25 -11
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/history.js +123 -0
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +10 -8
- package/dist/src/duck/kept-route.js +82 -2
- package/dist/src/duck/kept.js +211 -44
- package/dist/src/duck/pgtree.js +40 -0
- package/dist/src/duck/plan.js +120 -30
- package/dist/src/duck/publication.js +6 -6
- package/dist/src/duck/requests.js +265 -93
- package/dist/src/duck/rewrite.js +1193 -73
- package/dist/src/duck/row-filter.js +649 -0
- package/dist/src/duck/store.js +271 -159
- package/dist/src/duck/stream.js +10 -5
- package/dist/src/duck/sync.js +907 -275
- package/dist/src/duck/types.js +6 -5
- package/dist/src/duck/usage.js +324 -0
- package/dist/src/duck/users.js +64 -15
- package/dist/src/duck/verified.js +9 -0
- package/dist/src/duck/views.js +2 -3
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/log-buffer.js +68 -0
- package/dist/src/lib/pg-numeric.js +5 -2
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/table-sync.js +267 -0
- package/dist/src/lib/timezone.js +3 -2
- package/dist/src/web/app.css +706 -55
- package/dist/src/web/app.js +330 -258
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +84 -15
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +446 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +434 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +218 -0
- package/dist/src/web/shared.js +568 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +10 -3
package/dist/src/duck/rewrite.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { pathIndex, SESSION_VARIABLE } from "./functions.js";
|
|
1
|
+
import { pathIndex, SESSION_VARIABLE, SETTINGS_VARIABLE } from "./functions.js";
|
|
2
2
|
import { cellsOf as cellsOfProgram, numericLiteral } from "../lib/pg-numeric.js";
|
|
3
|
-
import { caseTwins } from "./pgtree.js";
|
|
4
|
-
import { canonicalRead, readKey } from "./kept-route.js";
|
|
5
|
-
import {
|
|
3
|
+
import { caseTwins, joinsUsing } from "./pgtree.js";
|
|
4
|
+
import { canonicalRead, EQUAL_STEPS, KEY_NULL_HEX, KEY_RECOMPUTE_HEX, MARKED_FUNCTIONS, markedError, ORDER_STEPS, readKey } from "./kept-route.js";
|
|
5
|
+
import { JSONB_KEY_FUNCTIONS } from "./jsonb-order.js";
|
|
6
|
+
import { atTimeZoneOf, isUtc, UTC_NAMES } from "../lib/timezone.js";
|
|
7
|
+
/** the DuckDB ICU collation a collation's strings are keyed by (`icu_sort_key`): ICU's own, or the one a libc order's
|
|
8
|
+
* accent and case levels are read with */
|
|
9
|
+
export const icuNameOf = (c) => c.kind === "icu" ? c.name : c.icu;
|
|
6
10
|
/** Whether pg_database's collation is a libc locale's (not C or POSIX), whose order is read from the database
|
|
7
11
|
* (src/duck/libc-order.ts) for `duckCollation`'s `libc` */
|
|
8
12
|
export const isLibcCollation = (provider, collate) => provider !== "b" && provider !== "i" && !/^(C|POSIX)([.].*)?$/i.test(collate);
|
|
@@ -22,12 +26,12 @@ export function duckCollation(provider, collate, locale, known) {
|
|
|
22
26
|
if (provider === "i") {
|
|
23
27
|
const name = icuName(locale ?? collate);
|
|
24
28
|
if (name === null)
|
|
25
|
-
throw new Error(`the source database's ICU locale ${JSON.stringify(locale ?? collate)} has no
|
|
29
|
+
throw new Error(`the source database's ICU locale ${JSON.stringify(locale ?? collate)} has no collation in the engine: Mason cannot sort text as it does`);
|
|
26
30
|
return { kind: "icu", name };
|
|
27
31
|
}
|
|
28
32
|
const name = icuName(collate);
|
|
29
33
|
if (name === null)
|
|
30
|
-
throw new Error(`the source database's collation ${JSON.stringify(collate)} has no
|
|
34
|
+
throw new Error(`the source database's collation ${JSON.stringify(collate)} has no collation in the engine to follow: Mason cannot sort text as it does`);
|
|
31
35
|
return { kind: "libc", icu: name };
|
|
32
36
|
}
|
|
33
37
|
export class Unrewritable extends Error {
|
|
@@ -49,9 +53,16 @@ const NUMBER_TYPES = new Set([INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC]);
|
|
|
49
53
|
const NUMBER_ELEMENT = { 1005: INT2, 1007: INT4, 1016: INT8, 1021: FLOAT4, 1022: FLOAT8, 1231: NUMERIC };
|
|
50
54
|
const TEXTS = new Set([TEXT, VARCHAR, BPCHAR, NAME, UNKNOWN]);
|
|
51
55
|
const JSONS = new Set([JSON_OID, JSONB]);
|
|
56
|
+
const JSONB_ARRAY = 3807;
|
|
57
|
+
/** the way out of a jsonb comparison Mason does not key (gap jsonb-compare) */
|
|
58
|
+
const JSONB_WAY_OUT = "compare a member (j ->> 'k'), or j::text for the canonical text";
|
|
52
59
|
const BOOL = 16, UUID = 2950, TIME = 1083, BYTEA = 17;
|
|
53
60
|
/** pg_type's array types of the element types above */
|
|
54
61
|
const ARRAYS = new Set([1000, 1001, 1005, 1007, 1009, 1014, 1015, 1016, 1021, 1022, 1028, 1115, 1182, 1183, 1185, 1187, 1231, 2951, 199, 3807]);
|
|
62
|
+
/** inet[], which the copy holds as a list of its text (src/duck/sync.ts KEPT_ARRAYS) */
|
|
63
|
+
const INET_ARRAY = 1041;
|
|
64
|
+
/** numeric[] and record[] */
|
|
65
|
+
const NUMERIC_ARRAY = 1231, RECORD_ARRAY = 2287;
|
|
55
66
|
/** pg_collation's C and POSIX: byte order */
|
|
56
67
|
const BYTE_COLLATIONS = new Set([950, 951]);
|
|
57
68
|
/** The DuckDB type a Postgres type is cast to (a typmod's numeric(p, s) included), or null where DuckDB has none. */
|
|
@@ -111,6 +122,8 @@ const arrayCast = (arg, element) => ({ TypeCast: { arg, typeName: { names: [S(el
|
|
|
111
122
|
const op = (name, l, r) => ({ A_Expr: { kind: "AEXPR_OP", name: [S(name)], ...(l === null ? {} : { lexpr: l }), rexpr: r } });
|
|
112
123
|
const not = (n) => ({ BoolExpr: { boolop: "NOT_EXPR", args: [n] } });
|
|
113
124
|
const and = (args) => ({ BoolExpr: { boolop: "AND_EXPR", args } });
|
|
125
|
+
/** the call that raises a marked row's error (src/duck/kept-route.ts `markedError`), over the column read */
|
|
126
|
+
const markedRaise = (name, column, e) => fn(name, [column, strConst(e.code), strConst(e.message)]);
|
|
114
127
|
const caseWhen = (arms, otherwise) => ({
|
|
115
128
|
CaseExpr: { args: arms.map(([expr, result]) => ({ CaseWhen: { expr, result } })), ...(otherwise === null ? {} : { defresult: otherwise }) },
|
|
116
129
|
});
|
|
@@ -173,6 +186,24 @@ class Rewriter {
|
|
|
173
186
|
answering = new Set();
|
|
174
187
|
/** the function calls a FROM reads (a set-returning function there is a table, not unnest's) */
|
|
175
188
|
inFrom = new WeakSet();
|
|
189
|
+
/** the set-returning functions in FROM a LEFT JOIN … ON TRUE reads, written as a list never empty (leftLateral) */
|
|
190
|
+
nonEmpty = new WeakSet();
|
|
191
|
+
/** such a function whose argument a step of its own before the lateral computes: the step's column (leftLateral) */
|
|
192
|
+
stepArg = new WeakMap();
|
|
193
|
+
/** the steps leftLateral wrote, each named for its number in the statement */
|
|
194
|
+
steps = 0;
|
|
195
|
+
/** the joins whose every column a `*` of the SELECT that holds them, or a join's own name, reads (markWholeReads):
|
|
196
|
+
* a step written into one would be a column of the answer (leftLateral) */
|
|
197
|
+
readWhole = new WeakSet();
|
|
198
|
+
/** the SELECTs and joins whose rows' order decides the answer (a LIMIT keeps the first of them, an aggregate or a
|
|
199
|
+
* window reads them in order): a LEFT lateral of a list is streamed there alone (markBounds, leftLateral) */
|
|
200
|
+
bounded = new WeakSet();
|
|
201
|
+
/** the same, were the statement's own rows bounded by a row count on the wire (an Execute's, markBounds): null when
|
|
202
|
+
* they are (the plan made for that Execute) */
|
|
203
|
+
boundedOnWire = null;
|
|
204
|
+
/** a LEFT lateral of a list left DuckDB's join that a row count on the wire would stream (leftLateral): the
|
|
205
|
+
* listener plans the statement again for an Execute that sets one (PlannedSql.unboundedLateral) */
|
|
206
|
+
unboundedLateral = false;
|
|
176
207
|
/** how the database changes a letter's case */
|
|
177
208
|
caseMode;
|
|
178
209
|
/** the ANY / ALL comparisons already guarded against a NULL array (the guard holds the comparison, walked again) */
|
|
@@ -190,7 +221,7 @@ class Rewriter {
|
|
|
190
221
|
/** every read of a copied table's JSON column by literal steps the statement makes, stored or not: the request log's
|
|
191
222
|
* `json_reads`, which the keeper's policy reads (src/duck/kept.ts `review`) */
|
|
192
223
|
jsonReads = new Map();
|
|
193
|
-
constructor(analysis, base, collation, userTypes = new Map(), rowTypes = new Map(), timeZone = "UTC", enums = { labels: new Map(), arrays: new Map() }, kept = null) {
|
|
224
|
+
constructor(analysis, base, collation, userTypes = new Map(), rowTypes = new Map(), timeZone = "UTC", enums = { labels: new Map(), arrays: new Map() }, kept = null, keyCounts = new Set()) {
|
|
194
225
|
this.types = analysis.types;
|
|
195
226
|
this.base = base;
|
|
196
227
|
this.collation = collation;
|
|
@@ -200,11 +231,83 @@ class Rewriter {
|
|
|
200
231
|
this.timeZone = timeZone;
|
|
201
232
|
this.enums = enums;
|
|
202
233
|
this.kept = kept;
|
|
234
|
+
for (const at of keyCounts)
|
|
235
|
+
this.keyCounts.add(at + base);
|
|
203
236
|
this.caseMode = collation.kind === "icu" ? "icu" : collation.kind === "bytes" && collation.ascii === true ? "ascii" : "libc";
|
|
204
237
|
this.twins = caseTwins(analysis.tree);
|
|
205
238
|
}
|
|
206
239
|
/** the subqueries and CTEs whose columns have twins in another case, by the name the query calls them (caseTwins) */
|
|
207
240
|
twins;
|
|
241
|
+
/**
|
|
242
|
+
* THE count(DISTINCT x) WHOSE DISTINCT CHANGES NOTHING, by the raw node's offset: x a column unique on the source and
|
|
243
|
+
* its table's rows not repeated by the FROM, decided over Postgres's analysis (src/duck/distinct-key.ts), and the
|
|
244
|
+
* same call repeated in its own SELECT's ORDER BY (`repeatedKeyCounts`). Each is counted without its DISTINCT
|
|
245
|
+
* (`funcCall`): DuckDB's hash aggregate of every value, which finds no duplicate, is what that saves.
|
|
246
|
+
*/
|
|
247
|
+
keyCounts = new Set();
|
|
248
|
+
/**
|
|
249
|
+
* An ORDER BY (or a window's) that repeats a count(DISTINCT x) of its SELECT list is analysed as THAT target
|
|
250
|
+
* (findTargetlistEntrySQL99): its own offset is in no AGGREF, so the decision never saw it. It is the same call over
|
|
251
|
+
* the same FROM, so it drops its DISTINCT where its twin in the SAME SELECT did — never by a twin in another SELECT,
|
|
252
|
+
* whose `x` may be another table's column.
|
|
253
|
+
*/
|
|
254
|
+
repeatedKeyCounts(root) {
|
|
255
|
+
if (this.keyCounts.size === 0)
|
|
256
|
+
return;
|
|
257
|
+
const select = (b) => {
|
|
258
|
+
const own = [];
|
|
259
|
+
const collect = (v) => {
|
|
260
|
+
if (Array.isArray(v)) {
|
|
261
|
+
for (const x of v)
|
|
262
|
+
collect(x);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (v === null || typeof v !== "object")
|
|
266
|
+
return;
|
|
267
|
+
if (isNode(v) && kindOf(v) === "SelectStmt") {
|
|
268
|
+
select(bodyOf(v));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (isNode(v) && kindOf(v) === "FuncCall" && bodyOf(v).agg_distinct === true && lastName(bodyOf(v).funcname) === "count")
|
|
272
|
+
own.push(bodyOf(v));
|
|
273
|
+
for (const [k, x] of Object.entries(isNode(v) ? bodyOf(v) : v)) {
|
|
274
|
+
const arm = armOf(k, x);
|
|
275
|
+
if (arm !== null)
|
|
276
|
+
select(bodyOf(arm));
|
|
277
|
+
else
|
|
278
|
+
collect(x);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
for (const [k, x] of Object.entries(b)) {
|
|
282
|
+
const arm = armOf(k, x);
|
|
283
|
+
if (arm !== null)
|
|
284
|
+
select(bodyOf(arm));
|
|
285
|
+
else
|
|
286
|
+
collect(x);
|
|
287
|
+
}
|
|
288
|
+
const dropped = new Set(own.filter((f) => this.keyCounts.has(f.location)).map((f) => shapeOf({ FuncCall: f })));
|
|
289
|
+
for (const f of own) {
|
|
290
|
+
if (typeof f.location === "number" && !this.keyCounts.has(f.location) && this.at(f, ["AGGREF"]) === null && dropped.has(shapeOf({ FuncCall: f })))
|
|
291
|
+
this.keyCounts.add(f.location);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const visit = (v) => {
|
|
295
|
+
if (Array.isArray(v)) {
|
|
296
|
+
for (const x of v)
|
|
297
|
+
visit(x);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (v === null || typeof v !== "object")
|
|
301
|
+
return;
|
|
302
|
+
if (isNode(v) && kindOf(v) === "SelectStmt") {
|
|
303
|
+
select(bodyOf(v));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
for (const x of Object.values(isNode(v) ? bodyOf(v) : v))
|
|
307
|
+
visit(x);
|
|
308
|
+
};
|
|
309
|
+
visit(root);
|
|
310
|
+
}
|
|
208
311
|
/**
|
|
209
312
|
* A STORED JSON PATH READ FROM ITS COLUMN (docs/DESIGN.md "Stored JSON paths"): `l` a copied table's
|
|
210
313
|
* json or jsonb column (Typed.relation) read by literal `steps` — recorded as one of the statement's reads (as its
|
|
@@ -238,11 +341,13 @@ class Rewriter {
|
|
|
238
341
|
const prefix = fields.length === 1 && t.qualifier !== null ? [S(t.qualifier)] : fields.slice(0, -1).map((f) => structuredClone(f));
|
|
239
342
|
const column = () => ({ ColumnRef: { fields: [...prefix.map((f) => structuredClone(f)), S(ready.name)] } });
|
|
240
343
|
const nul = () => fn("chr", [intConst(0)]);
|
|
344
|
+
const recompute = () => op("||", nul(), strConst("!"));
|
|
241
345
|
const value = asText ? keptText(column, canonical) : caseWhen([[op("=", column(), nul()), nullConst()]], column());
|
|
346
|
+
// a row marked since the path was counted exact is never read as its value: it raises (kept-route.ts markedError)
|
|
242
347
|
if (ready.exact)
|
|
243
|
-
return value;
|
|
348
|
+
return caseWhen([[op("=", column(), recompute()), markedRaise(MARKED_FUNCTIONS.value, column(), markedError(read, false))]], value);
|
|
244
349
|
// (a copied table's column, checked above: a jsonb's text is the copy's canonical one)
|
|
245
|
-
return caseWhen([[or([isNull(column()), op("=", column(),
|
|
350
|
+
return caseWhen([[or([isNull(column()), op("=", column(), recompute())]), jsonGet(this.cp, l, steps, either, canonical, asText, canonical)]], value);
|
|
246
351
|
}
|
|
247
352
|
/**
|
|
248
353
|
* A JSON READ OFF `base` BY `steps`: from its stored column where every row holds it (keptRead), else DuckDB's own
|
|
@@ -370,6 +475,10 @@ class Rewriter {
|
|
|
370
475
|
const own = isNode(v) ? this.synthetic.get(bodyOf(v)) : undefined;
|
|
371
476
|
if (own !== undefined)
|
|
372
477
|
this.synthetic.set(bodyOf(out), own);
|
|
478
|
+
// and one typed as another (decidedByKey's any_value): a grouped ORDER BY 1 over a jsonb is keyed over its copy
|
|
479
|
+
const like = isNode(v) ? this.typedLike.get(bodyOf(v)) : undefined;
|
|
480
|
+
if (like !== undefined)
|
|
481
|
+
this.typedLike.set(bodyOf(out), like);
|
|
373
482
|
return out;
|
|
374
483
|
}
|
|
375
484
|
walk(v) {
|
|
@@ -449,9 +558,33 @@ class Rewriter {
|
|
|
449
558
|
case "ParamRef": return this.param(b);
|
|
450
559
|
case "SortBy": return this.sortBy(b, null, null);
|
|
451
560
|
case "RangeFunction": return this.rangeFunction(b);
|
|
561
|
+
case "JoinExpr": return this.leftLateral(b);
|
|
452
562
|
case "CollateClause": return this.collateClause(b);
|
|
453
563
|
case "MinMaxExpr": return this.minMax(b);
|
|
454
564
|
case "SQLValueFunction": return this.sqlValueFunction(b);
|
|
565
|
+
// CASE x WHEN v …: each WHEN is x = v, jsonb's equality for a jsonb (its keys compared)
|
|
566
|
+
case "CaseExpr": {
|
|
567
|
+
if (!isNode(b.arg) || (this.typeOf(b.arg) !== JSONB && this.typeOf(b.arg) !== JSONB_ARRAY))
|
|
568
|
+
return null;
|
|
569
|
+
b.arg = this.equated(b.arg, "CASE … WHEN of");
|
|
570
|
+
for (const w of (b.args ?? []))
|
|
571
|
+
if (isNode(bodyOf(w).expr))
|
|
572
|
+
bodyOf(w).expr = this.equalKey(bodyOf(w).expr);
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
// x IN (SELECT …), x = ANY / ALL (SELECT …): the subquery's values compared with x by jsonb's equality
|
|
576
|
+
case "SubLink": {
|
|
577
|
+
if (b.subLinkType === "ARRAY_SUBLINK") {
|
|
578
|
+
this.arraySubquery(b);
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
if (!isNode(b.testexpr) || !["ANY_SUBLINK", "ALL_SUBLINK", "ROWCOMPARE_SUBLINK"].includes(String(b.subLinkType)))
|
|
582
|
+
return null;
|
|
583
|
+
const type = this.typeOf(b.testexpr);
|
|
584
|
+
if (type === JSONB || type === JSONB_ARRAY || this.rowHoldsJsonb(b.testexpr))
|
|
585
|
+
throw new Unrewritable("a jsonb compared with a subquery's values (IN (SELECT …), = ANY (SELECT …)) is not supported by Mason (Postgres equates jsonb by its own rules)", "join the subquery instead: … JOIN (SELECT …) s ON s.v = t.j", "jsonb-compare");
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
455
588
|
// a row as a value anywhere but the SELECT list, a cast to text and the json functions (rowValues) is a struct
|
|
456
589
|
// to DuckDB, compared, tested and grouped by its rules
|
|
457
590
|
case "ColumnRef":
|
|
@@ -465,6 +598,11 @@ class Rewriter {
|
|
|
465
598
|
// and a column its subquery has twice in two cases (`"T"`, `t`) is that subquery's renamed one (caseTwins)
|
|
466
599
|
const fields = (b.fields ?? []);
|
|
467
600
|
const last = fields.at(-1);
|
|
601
|
+
// a function's whole row by its FROM name alone, where the function is one column and no row (`e` of
|
|
602
|
+
// `jsonb_array_elements(x) e`, whose column is `value`): that column, which DuckDB would read as a struct
|
|
603
|
+
const whole = k === "ColumnRef" && t?.node === "VAR" && fields.length === 1 && last !== undefined && kindOf(last) === "String" ? t.functionColumn : undefined;
|
|
604
|
+
if (whole !== undefined && String(bodyOf(last).sval) !== whole.column)
|
|
605
|
+
return { ColumnRef: { ...b, fields: [S(whole.alias), S(t.renamed ?? whole.column)] } };
|
|
468
606
|
if (k === "ColumnRef" && t?.node === "VAR" && last !== undefined && kindOf(last) === "String" && (t.renamed !== null || (t.qualifier !== null && fields.length === 1))) {
|
|
469
607
|
const prefix = fields.length === 1 && t.qualifier !== null ? [S(t.qualifier)] : fields.slice(0, -1);
|
|
470
608
|
return { ColumnRef: { ...b, fields: [...prefix, t.renamed !== null ? S(t.renamed) : last] } };
|
|
@@ -535,6 +673,42 @@ class Rewriter {
|
|
|
535
673
|
default: return null;
|
|
536
674
|
}
|
|
537
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* ARRAY(SELECT x …) answers as Postgres's only where x is a scalar DuckDB holds as Postgres does (text, an integer,
|
|
678
|
+
* jsonb, an enum, a bool, a date, …): a numeric is DuckDB's double there (Postgres's numeric[] is exact), an array
|
|
679
|
+
* is a list of lists (Postgres accumulates a multi-dimensional array, and raises 2202E on an empty one, 22004 on a
|
|
680
|
+
* NULL), a row a struct (record[] to Postgres). Those, and an element whose type the analysis does not say, are
|
|
681
|
+
* refused by name.
|
|
682
|
+
*/
|
|
683
|
+
arraySubquery(b) {
|
|
684
|
+
let q = isNode(b.subselect) ? bodyOf(b.subselect) : undefined;
|
|
685
|
+
while (q !== undefined && q.op !== undefined && q.op !== "SETOP_NONE" && q.larg !== undefined)
|
|
686
|
+
q = q.larg;
|
|
687
|
+
const target = (q?.targetList ?? [])[0];
|
|
688
|
+
const val = target !== undefined ? bodyOf(target).val : undefined;
|
|
689
|
+
let t = isNode(val) ? this.typeOf(val) : null;
|
|
690
|
+
// an array's element (`a[1]`: no offset of its own to the analysis) is a scalar of the array's element type, a
|
|
691
|
+
// numeric where the array is a numeric[]; a slice is an array
|
|
692
|
+
if (t === null && isNode(val) && kindOf(val) === "A_Indirection") {
|
|
693
|
+
const ind = (bodyOf(val).indirection ?? []);
|
|
694
|
+
const at = isNode(bodyOf(val).arg) ? this.typeOf(bodyOf(val).arg) : null;
|
|
695
|
+
// (a user type's array other than an enum's may be a table's rows: an element whose type is not told)
|
|
696
|
+
const told = at !== null && at !== RECORD_ARRAY && (this.userTypes.get(at)?.startsWith("_") !== true || this.enums.arrays.has(at));
|
|
697
|
+
if (told && ind.length > 0 && ind.every((x) => kindOf(x) === "A_Indices" && bodyOf(x).is_slice !== true))
|
|
698
|
+
t = at === NUMERIC_ARRAY ? NUMERIC : TEXT;
|
|
699
|
+
}
|
|
700
|
+
const refuse = (what) => {
|
|
701
|
+
throw new Unrewritable(`ARRAY(SELECT …) of ${what} is not supported by Mason`, "array_agg(x ORDER BY …) in a subquery, or ARRAY(SELECT x::text …)", "array-subquery-element");
|
|
702
|
+
};
|
|
703
|
+
if (t === null)
|
|
704
|
+
refuse("a value whose type Mason cannot tell");
|
|
705
|
+
if (t === NUMERIC)
|
|
706
|
+
refuse("a numeric (Postgres's numeric[] is exact)");
|
|
707
|
+
if (ARRAYS.has(t) || this.enums.arrays.has(t) || t === INET_ARRAY || this.userTypes.get(t)?.startsWith("_") === true)
|
|
708
|
+
refuse("an array (Postgres accumulates a multi-dimensional array)");
|
|
709
|
+
if (this.isRowType(t))
|
|
710
|
+
refuse("a row (Postgres's record[])");
|
|
711
|
+
}
|
|
538
712
|
isRowType(oid) { return oid === RECORD || this.userTypes.get(oid)?.startsWith("rowtype:") === true; }
|
|
539
713
|
/** a typed reference to a FROM item's column (a row's, read as its columns) */
|
|
540
714
|
typedRef(names, type, typmod) {
|
|
@@ -565,7 +739,7 @@ class Rewriter {
|
|
|
565
739
|
if (table !== undefined)
|
|
566
740
|
return table.map((c) => ({ name: c.name, value: this.typedRef([alias, c.name], c.type, c.typmod), type: c.type }));
|
|
567
741
|
if (t.type !== RECORD)
|
|
568
|
-
throw new Unrewritable(`a row of ${this.userTypes.get(t.type)?.replace(/^rowtype:/, "") ?? "a table"} is not supported by Mason: Mason
|
|
742
|
+
throw new Unrewritable(`a row of ${this.userTypes.get(t.type)?.replace(/^rowtype:/, "") ?? "a table"} is not supported by Mason: Mason synced the table in part`, "name its columns instead", "row-value");
|
|
569
743
|
const source = this.fromSources(select).filter((q) => q.alias === alias);
|
|
570
744
|
if (source.length !== 1)
|
|
571
745
|
throw new Unrewritable("a row of this FROM item is not supported by Mason", "name its columns instead", "row-value");
|
|
@@ -662,6 +836,9 @@ class Rewriter {
|
|
|
662
836
|
if (isNode(c) && typeof bodyOf(c).ctename === "string" && isNode(bodyOf(c).ctequery))
|
|
663
837
|
this.ctes.set(bodyOf(c).ctename, bodyOf(c).ctequery);
|
|
664
838
|
this.rowValues(b);
|
|
839
|
+
for (const w of (b.windowClause ?? []))
|
|
840
|
+
if (isNode(w))
|
|
841
|
+
this.keyedPartition(bodyOf(w));
|
|
665
842
|
// a GROUP BY of an output name is grouped by its ordinal: DuckDB binds a name without regard to case, so `"Name"`
|
|
666
843
|
// would be another table's `name` (the corpus run of fbbf8f08), where Postgres finds no input column of that name
|
|
667
844
|
if (Array.isArray(b.groupClause) && Array.isArray(b.targetList)) {
|
|
@@ -674,22 +851,33 @@ class Rewriter {
|
|
|
674
851
|
return at >= 0 && targets.filter((t) => bodyOf(t).name === f[0]).length === 1 ? intConst(at + 1) : g;
|
|
675
852
|
});
|
|
676
853
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (Array.isArray(b.distinctClause))
|
|
681
|
-
for (const t of (b.targetList ?? []))
|
|
682
|
-
if (isNode(bodyOf(t).val))
|
|
683
|
-
this.comparable(bodyOf(t).val, "SELECT DISTINCT");
|
|
684
|
-
if (b.op !== undefined && b.op !== "SETOP_NONE" && b.all !== true) {
|
|
854
|
+
this.keyedGroups(b);
|
|
855
|
+
// a set operation compares its rows (without ALL, and INTERSECT ALL / EXCEPT ALL count them by equality)
|
|
856
|
+
if (b.op !== undefined && b.op !== "SETOP_NONE" && (b.all !== true || b.op !== "SETOP_UNION")) {
|
|
685
857
|
let first = b;
|
|
686
858
|
while (first.larg !== undefined)
|
|
687
859
|
first = first.larg;
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
860
|
+
const what = `${String(b.op).replace("SETOP_", "")}${b.all === true ? " ALL" : " (without ALL)"} of`;
|
|
861
|
+
for (const t of (first.targetList ?? [])) {
|
|
862
|
+
const v = bodyOf(t).val;
|
|
863
|
+
if (!isNode(v))
|
|
864
|
+
continue;
|
|
865
|
+
if (this.starHoldsJsonb(v))
|
|
866
|
+
throw new Unrewritable(`${what} a * holding a jsonb column is not supported by Mason (Postgres equates jsonb by its own rules)`, JSONB_WAY_OUT, "jsonb-compare");
|
|
867
|
+
// (INTERSECT ALL and EXCEPT ALL: jsonb's equality alone, which is not its text's; the rest as before)
|
|
868
|
+
if (b.all !== true || this.typeOf(v) === JSONB || this.typeOf(v) === JSONB_ARRAY)
|
|
869
|
+
this.comparable(v, what);
|
|
870
|
+
}
|
|
691
871
|
}
|
|
692
872
|
this.decidedByKey(b);
|
|
873
|
+
// `x IS NULL, x DESC` as `x DESC NULLS LAST` (nullsKeyFolded): the folded key is the statement's own, its rules to run
|
|
874
|
+
if (Array.isArray(b.sortClause)) {
|
|
875
|
+
b.sortClause = nullsKeyFolded(b.sortClause, b);
|
|
876
|
+
for (const s of b.sortClause)
|
|
877
|
+
this.original.add(bodyOf(s));
|
|
878
|
+
}
|
|
879
|
+
// (after decidedByKey: a DISTINCT ON a target names it as the SELECT list reads it, through any_value where it must)
|
|
880
|
+
this.keyedDistinct(b);
|
|
693
881
|
// the statement's own ORDER BY: an ordinal or an output name sorts by that target's expression, so its key is
|
|
694
882
|
// built over the expression (DuckDB's `ORDER BY 1 COLLATE …` would sort by the constant)
|
|
695
883
|
if (Array.isArray(b.sortClause) && Array.isArray(b.targetList) && (b.op === undefined || b.op === "SETOP_NONE")) {
|
|
@@ -697,6 +885,12 @@ class Rewriter {
|
|
|
697
885
|
b.sortClause = b.sortClause.flatMap((s) => {
|
|
698
886
|
const sb = bodyOf(s);
|
|
699
887
|
this.done.add(sb);
|
|
888
|
+
// an ordinal that names a jsonb a `*` lists: DuckDB would sort that column's text
|
|
889
|
+
const n = constInt(sb.node);
|
|
890
|
+
const starred = n === null ? null : this.starColumnType(targets, n);
|
|
891
|
+
if (starred === JSONB || starred === JSONB_ARRAY) {
|
|
892
|
+
throw new Unrewritable("ORDER BY the number of a jsonb column a * lists is not supported by Mason (Postgres orders jsonb by its own rules)", "ORDER BY the column's name", "jsonb-compare");
|
|
893
|
+
}
|
|
700
894
|
return this.sortBy(sb, targets, b) ?? [s];
|
|
701
895
|
});
|
|
702
896
|
}
|
|
@@ -740,7 +934,9 @@ class Rewriter {
|
|
|
740
934
|
const target = bodyOf(targets[n - 1]);
|
|
741
935
|
const value = target.val;
|
|
742
936
|
const c = this.textCollation(value);
|
|
743
|
-
|
|
937
|
+
// (a jsonb is sorted by its key whatever the collation: the SortBy rule keys the output column)
|
|
938
|
+
const keyed = this.typeOf(value) === JSONB || this.typeOf(value) === JSONB_ARRAY;
|
|
939
|
+
if (!keyed && (c === null || c.kind === "bytes"))
|
|
744
940
|
continue;
|
|
745
941
|
if (target.name === undefined)
|
|
746
942
|
target.name = `__mason_sort_${n}`;
|
|
@@ -849,7 +1045,7 @@ class Rewriter {
|
|
|
849
1045
|
*/
|
|
850
1046
|
withTies(b) {
|
|
851
1047
|
if (b.distinctClause !== undefined)
|
|
852
|
-
throw new Unrewritable("FETCH … WITH TIES over a SELECT DISTINCT is not supported by Mason (
|
|
1048
|
+
throw new Unrewritable("FETCH … WITH TIES over a SELECT DISTINCT is not supported by Mason (the engine has no WITH TIES)", "select the DISTINCT rows in a subquery and FETCH … WITH TIES from it", "with-ties-distinct");
|
|
853
1049
|
const order = b.sortClause ?? [];
|
|
854
1050
|
const count = b.limitCount;
|
|
855
1051
|
delete b.limitCount;
|
|
@@ -1637,6 +1833,9 @@ class Rewriter {
|
|
|
1637
1833
|
e = this.foldCitext(e);
|
|
1638
1834
|
b.node = e;
|
|
1639
1835
|
}
|
|
1836
|
+
// a jsonb sorts by its order key (jsonb's own order: src/duck/jsonb-order.ts), its NULLs where the SortBy puts them
|
|
1837
|
+
if (type === JSONB)
|
|
1838
|
+
return [{ SortBy: { ...b, node: this.jsonbKey(e) } }];
|
|
1640
1839
|
this.comparable(e, "ORDER BY");
|
|
1641
1840
|
const c = this.textCollation(e);
|
|
1642
1841
|
// a char(n) sorts without its padding (bpcharcmp)
|
|
@@ -1707,17 +1906,217 @@ class Rewriter {
|
|
|
1707
1906
|
}
|
|
1708
1907
|
return g;
|
|
1709
1908
|
}
|
|
1710
|
-
/** a value Mason cannot sort, group or compare as Postgres does: jsonb
|
|
1711
|
-
*
|
|
1909
|
+
/** a value Mason cannot sort, group or compare as Postgres does: a jsonb where it is not keyed (`compared` keys it),
|
|
1910
|
+
* a jsonb array (each element by jsonb's rules), a text array under the database's collation (each element under
|
|
1911
|
+
* it) — refused, where a statement would need it */
|
|
1712
1912
|
comparable(n, where) {
|
|
1713
1913
|
const type = this.typeOf(n);
|
|
1714
1914
|
if (this.isCitext(type))
|
|
1715
1915
|
throw new Unrewritable(`${where} a citext value is not supported by Mason (Postgres groups and sorts it without regard to case)`, "GROUP BY lower(x), or x::text where case matters", "citext-forms");
|
|
1716
1916
|
if (type === JSONB)
|
|
1717
|
-
throw new Unrewritable(`${where} a jsonb value is not supported by Mason (Postgres orders and equates jsonb by its own rules)`,
|
|
1917
|
+
throw new Unrewritable(`${where} a jsonb value is not supported by Mason (Postgres orders and equates jsonb by its own rules)`, JSONB_WAY_OUT, "jsonb-compare");
|
|
1918
|
+
if (type === JSONB_ARRAY)
|
|
1919
|
+
throw new Unrewritable(`${where} a jsonb array is not supported by Mason (Postgres compares its elements by jsonb's rules)`, "compare an element (a[1]), or a::text", "jsonb-compare");
|
|
1920
|
+
if (this.rowHoldsJsonb(n))
|
|
1921
|
+
throw new Unrewritable(`${where} a row holding a jsonb is not supported by Mason (Postgres compares the jsonb by its own rules)`, "list its members one by one: GROUP BY a, j", "jsonb-compare");
|
|
1718
1922
|
if ((type === 1009 || type === 1015) && this.collation.kind !== "bytes")
|
|
1719
1923
|
throw new Unrewritable(`${where} a text array is not supported by Mason under the database's collation`, "sort by an element (a[1]) or by array_to_string(a, ',')", "text-array-order");
|
|
1720
1924
|
}
|
|
1925
|
+
/**
|
|
1926
|
+
* A JSONB AS ITS ORDER KEY (src/duck/jsonb-order.ts): bytes whose order and equality are Postgres's for jsonb
|
|
1927
|
+
* (`compareJsonbContainers`: by type, then size, then member by member; numbers as numerics; strings under the
|
|
1928
|
+
* database's collation). Under C the key is one call; under ICU or a libc locale each of the document's strings
|
|
1929
|
+
* needs DuckDB's ICU key, which a JavaScript function cannot ask for, so DuckDB makes them over the strings the
|
|
1930
|
+
* function lists (`list_transform` with a lambda, src/duck/store.ts `lambda_syntax`) and hands them to the key.
|
|
1931
|
+
*/
|
|
1932
|
+
jsonbKey(e) {
|
|
1933
|
+
return this.keptKey(e) ?? this.jsonbKeyCall(e);
|
|
1934
|
+
}
|
|
1935
|
+
/** the key computed per row (`jsonbKey`): the functions src/duck/jsonb-order.ts `JSONB_KEY_FUNCTIONS` names */
|
|
1936
|
+
jsonbKeyCall(e) {
|
|
1937
|
+
const c = this.collation;
|
|
1938
|
+
if (c.kind === "bytes")
|
|
1939
|
+
return fn(JSONB_KEY_FUNCTIONS.bytes.key, [e]);
|
|
1940
|
+
const each = () => ({ ColumnRef: { fields: [S("__mason_s")] } });
|
|
1941
|
+
const f = JSONB_KEY_FUNCTIONS[c.kind];
|
|
1942
|
+
const keys = fn("list_transform", [fn(f.strings, [this.clone(e)]), op("->", each(), fn("icu_sort_key", [each(), strConst(icuNameOf(c))]))]);
|
|
1943
|
+
return fn(f.key, [e, keys]);
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* A JSONB'S STORED KEY (src/duck/kept-route.ts `ORDER_STEPS`): `e` a copied table's jsonb column — recorded as one of
|
|
1947
|
+
* the statement's reads, the order's, so the keeper weighs storing its key as it weighs a path — read from the key's
|
|
1948
|
+
* hidden column once every row of it is stored (READY). The column holds the key, `KEY_NULL` for a NULL document, or
|
|
1949
|
+
* `KEY_RECOMPUTE` where the key raised; read alone while no row is marked, else through a CASE that computes the
|
|
1950
|
+
* marked rows (and raises the key's own error). null: not such a column, or not stored — the caller computes the key.
|
|
1951
|
+
* `steps` `EQUAL_STEPS` is the C key under a libc collation (`equalKey`), recorded and read the same way, computed by
|
|
1952
|
+
* the C key's function where a row is marked.
|
|
1953
|
+
*/
|
|
1954
|
+
keptKey(e, steps = ORDER_STEPS) {
|
|
1955
|
+
// a column of a derived table typed as the subquery's target (sortBy's `s.*` wrapper, `typedLike`) is the
|
|
1956
|
+
// subquery's column, whose FROM item has no hidden column (the oracle's eager run: DuckDB's 42703)
|
|
1957
|
+
if (this.kept === null || kindOf(e) !== "ColumnRef" || this.typedLike.has(bodyOf(e)))
|
|
1958
|
+
return null;
|
|
1959
|
+
const fields = (bodyOf(e).fields ?? []);
|
|
1960
|
+
const last = fields.at(-1);
|
|
1961
|
+
const t = this.typed(e);
|
|
1962
|
+
if (last === undefined || kindOf(last) !== "String" || t === null || t.node !== "VAR" || t.relation === undefined || t.renamed !== null)
|
|
1963
|
+
return null;
|
|
1964
|
+
const at = this.kept.at(t.relation.relid, t.relation.attno);
|
|
1965
|
+
if (at === null)
|
|
1966
|
+
return null;
|
|
1967
|
+
const read = { source: at.source, column: at.column, steps, either: false, canonical: true, asText: false };
|
|
1968
|
+
this.jsonReads.set(readKey(read), read);
|
|
1969
|
+
const ready = this.kept.ready(read);
|
|
1970
|
+
if (ready === null)
|
|
1971
|
+
return null;
|
|
1972
|
+
const computed = () => steps === EQUAL_STEPS ? fn(JSONB_KEY_FUNCTIONS.bytes.key, [e]) : this.jsonbKeyCall(e);
|
|
1973
|
+
const prefix = fields.length === 1 && t.qualifier !== null ? [S(t.qualifier)] : fields.slice(0, -1).map((f) => structuredClone(f));
|
|
1974
|
+
const column = () => ({ ColumnRef: { fields: [...prefix.map((f) => structuredClone(f)), S(ready.name)] } });
|
|
1975
|
+
const recompute = () => fn("unhex", [strConst(KEY_RECOMPUTE_HEX)]);
|
|
1976
|
+
const isNullKey = () => op("=", column(), fn("unhex", [strConst(KEY_NULL_HEX)]));
|
|
1977
|
+
// a row marked since the key was counted exact is never read as a key: it raises (kept-route.ts markedError)
|
|
1978
|
+
if (ready.exact) {
|
|
1979
|
+
const raise = markedRaise(MARKED_FUNCTIONS.key, fn("hex", [column()]), markedError(read, this.collation.kind === "libc"));
|
|
1980
|
+
return caseWhen([[isNullKey(), nullConst()], [op("=", column(), recompute()), raise]], column());
|
|
1981
|
+
}
|
|
1982
|
+
return caseWhen([[or([isNull(column()), op("=", column(), recompute())]), computed()]], caseWhen([[isNullKey(), nullConst()]], column()));
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* A JSONB AS ITS EQUALITY KEY: the key under C, whatever the database's collation. Postgres's default collation is
|
|
1986
|
+
* deterministic, so two jsonb strings are equal only when their bytes are (varstr_cmp breaks a collation's tie by
|
|
1987
|
+
* strcmp), and the C key's equality is exactly jsonb's — where the collation's key would refuse a value its order
|
|
1988
|
+
* does not cover (a libc collation's `libc-uncovered`: an emoji's variation selector, an NFD accent) that equality
|
|
1989
|
+
* never needed to order. Every context that only equates (=, <>, IN, = ANY, IS DISTINCT FROM, NULLIF, CASE x WHEN,
|
|
1990
|
+
* GROUP BY, DISTINCT [ON], count(DISTINCT), PARTITION BY) reads this one; ORDER BY, <, >, BETWEEN read `jsonbKey`.
|
|
1991
|
+
*/
|
|
1992
|
+
equalKey(e) {
|
|
1993
|
+
// under C and ICU the order's key is an equality key too (a string's collation key, then its bytes), which never
|
|
1994
|
+
// refuses, so a stored one serves (keptKey) — both sides of an equality read the order's key; only a libc key
|
|
1995
|
+
// refuses a string (libc-uncovered), so under libc equality reads the C key: its own stored column where it is
|
|
1996
|
+
// (`EQUAL_STEPS`, src/duck/kept.ts), else computed
|
|
1997
|
+
if (this.collation.kind !== "libc")
|
|
1998
|
+
return this.jsonbKey(e);
|
|
1999
|
+
return this.keptKey(e, EQUAL_STEPS) ?? fn(JSONB_KEY_FUNCTIONS.bytes.key, [e]);
|
|
2000
|
+
}
|
|
2001
|
+
/** a value compared for EQUALITY only (PARTITION BY, IS DISTINCT FROM, NULLIF, CASE x WHEN, = ANY, DISTINCT ON): a
|
|
2002
|
+
* jsonb as its equality key, a jsonb array refused; any other value as it is (its equality is DuckDB's) */
|
|
2003
|
+
equated(n, where) {
|
|
2004
|
+
const type = this.typeOf(n);
|
|
2005
|
+
if (type === JSONB)
|
|
2006
|
+
return this.equalKey(n);
|
|
2007
|
+
if (type === JSONB_ARRAY)
|
|
2008
|
+
this.comparable(n, where);
|
|
2009
|
+
return n;
|
|
2010
|
+
}
|
|
2011
|
+
/** a row value (`(a, j)`, `ROW(…)`, a list of IN's left) one of whose members, at any depth, is a jsonb or a jsonb array */
|
|
2012
|
+
rowHoldsJsonb(n) {
|
|
2013
|
+
if (kindOf(n) !== "RowExpr")
|
|
2014
|
+
return false;
|
|
2015
|
+
return (bodyOf(n).args ?? []).some((a) => isNode(a) && (this.typeOf(a) === JSONB || this.typeOf(a) === JSONB_ARRAY || this.rowHoldsJsonb(a)));
|
|
2016
|
+
}
|
|
2017
|
+
/** the type of the `n`th output column where a `*` (or `t.*`) lists it, as Postgres expanded the stars (each column's
|
|
2018
|
+
* VAR at its star's offset, in order); null where the column is a target of its own, or not known */
|
|
2019
|
+
starColumnType(targets, n) {
|
|
2020
|
+
let at = 0;
|
|
2021
|
+
for (const t of targets) {
|
|
2022
|
+
const v = bodyOf(t).val;
|
|
2023
|
+
if (v === undefined || !hasStar(v) || kindOf(v) !== "ColumnRef") {
|
|
2024
|
+
at++;
|
|
2025
|
+
if (at === n)
|
|
2026
|
+
return null;
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
const loc = bodyOf(v).location;
|
|
2030
|
+
const cols = typeof loc === "number" ? (this.types.get(loc - this.base) ?? []).filter((x) => x.node === "VAR") : [];
|
|
2031
|
+
if (cols.length === 0)
|
|
2032
|
+
return null;
|
|
2033
|
+
if (n <= at + cols.length)
|
|
2034
|
+
return cols[n - at - 1].type;
|
|
2035
|
+
at += cols.length;
|
|
2036
|
+
}
|
|
2037
|
+
return null;
|
|
2038
|
+
}
|
|
2039
|
+
/** a `*` (or `t.*`) whose columns hold a jsonb or a jsonb array, as Postgres expanded it (each column's VAR at the
|
|
2040
|
+
* star's own offset) */
|
|
2041
|
+
starHoldsJsonb(n) {
|
|
2042
|
+
if (!hasStar(n) || kindOf(n) !== "ColumnRef")
|
|
2043
|
+
return false;
|
|
2044
|
+
const at = bodyOf(n).location;
|
|
2045
|
+
return typeof at === "number" && (this.types.get(at - this.base) ?? []).some((t) => t.node === "VAR" && (t.type === JSONB || t.type === JSONB_ARRAY));
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* GROUP BY A JSONB (Postgres groups by jsonb's equality: `1.0` and `1.00` are one group): grouped by its order key,
|
|
2049
|
+
* an ordinal or an output name by its target's. The value itself is then no group key, so `decidedByKey` reads it
|
|
2050
|
+
* through any_value — the group's value, which is one of the texts Postgres's group holds, as Postgres's answer is
|
|
2051
|
+
* the one its plan meets first (gap jsonb-equal-text). Anything else Mason cannot group as Postgres does is refused,
|
|
2052
|
+
* in a grouping set too.
|
|
2053
|
+
*/
|
|
2054
|
+
keyedGroups(b) {
|
|
2055
|
+
if (!Array.isArray(b.groupClause))
|
|
2056
|
+
return;
|
|
2057
|
+
const refuseIn = (v) => {
|
|
2058
|
+
if (Array.isArray(v)) {
|
|
2059
|
+
for (const x of v)
|
|
2060
|
+
refuseIn(x);
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
if (!isNode(v))
|
|
2064
|
+
return;
|
|
2065
|
+
if (kindOf(v) === "GroupingSet")
|
|
2066
|
+
refuseIn(bodyOf(v).content);
|
|
2067
|
+
else
|
|
2068
|
+
this.comparable(this.groupedExpression(v, b), "GROUP BY a grouping set of");
|
|
2069
|
+
};
|
|
2070
|
+
b.groupClause = b.groupClause.map((g) => {
|
|
2071
|
+
if (!isNode(g))
|
|
2072
|
+
return g;
|
|
2073
|
+
if (kindOf(g) === "GroupingSet") {
|
|
2074
|
+
refuseIn(bodyOf(g).content);
|
|
2075
|
+
return g;
|
|
2076
|
+
}
|
|
2077
|
+
const e = this.groupedExpression(g, b);
|
|
2078
|
+
if (this.typeOf(e) !== JSONB) {
|
|
2079
|
+
this.comparable(e, "GROUP BY");
|
|
2080
|
+
return g;
|
|
2081
|
+
}
|
|
2082
|
+
return this.equalKey(e === g ? g : this.clone(e));
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* SELECT DISTINCT OVER A JSONB (Postgres's DISTINCT is jsonb's equality): DuckDB's DISTINCT ON the targets, each
|
|
2087
|
+
* jsonb by its order key — the first row of each set of equal ones, in the ORDER BY's order, as DuckDB's DISTINCT ON
|
|
2088
|
+
* keeps (one of the texts Postgres's answer may be: gap jsonb-equal-text). A DISTINCT ON's own jsonb expression is
|
|
2089
|
+
* keyed alike. A `*` holding a jsonb column is refused (its columns are not written out here).
|
|
2090
|
+
*/
|
|
2091
|
+
keyedDistinct(b) {
|
|
2092
|
+
if (!Array.isArray(b.distinctClause))
|
|
2093
|
+
return;
|
|
2094
|
+
const targets = (b.targetList ?? []);
|
|
2095
|
+
const plain = b.distinctClause.length === 1 && !isNode(b.distinctClause[0]);
|
|
2096
|
+
const values = targets.map((t) => bodyOf(t).val).filter(isNode);
|
|
2097
|
+
for (const v of values)
|
|
2098
|
+
if (this.typeOf(v) !== JSONB)
|
|
2099
|
+
this.comparable(v, "SELECT DISTINCT");
|
|
2100
|
+
if (!plain) {
|
|
2101
|
+
b.distinctClause = b.distinctClause.map((d) => {
|
|
2102
|
+
const n = constInt(d);
|
|
2103
|
+
const ordinal = n !== null && n >= 1 && n <= targets.length;
|
|
2104
|
+
const e = ordinal ? bodyOf(targets[n - 1]).val : d;
|
|
2105
|
+
if (this.typeOf(e) !== JSONB) {
|
|
2106
|
+
this.comparable(e, "SELECT DISTINCT ON");
|
|
2107
|
+
return d;
|
|
2108
|
+
}
|
|
2109
|
+
return this.equalKey(ordinal ? this.clone(e) : d);
|
|
2110
|
+
});
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
for (const v of values)
|
|
2114
|
+
if (this.starHoldsJsonb(v))
|
|
2115
|
+
throw new Unrewritable("SELECT DISTINCT * over a jsonb column is not supported by Mason (Postgres equates jsonb by its own rules)", "name the columns: SELECT DISTINCT a, b, …", "jsonb-compare");
|
|
2116
|
+
if (!values.some((v) => this.typeOf(v) === JSONB))
|
|
2117
|
+
return;
|
|
2118
|
+
b.distinctClause = values.map((v) => this.equated(this.clone(v), "SELECT DISTINCT"));
|
|
2119
|
+
}
|
|
1721
2120
|
collateClause(b) {
|
|
1722
2121
|
// an expression's own COLLATE outside a sort: a comparison's is handled at the comparison; C needs nothing
|
|
1723
2122
|
const c = this.namedCollation(lastName(b.collname));
|
|
@@ -1740,6 +2139,9 @@ class Rewriter {
|
|
|
1740
2139
|
minMax(b) {
|
|
1741
2140
|
// GREATEST / LEAST of text compare under the collation
|
|
1742
2141
|
const t = this.at(b, ["MINMAXEXPR"]);
|
|
2142
|
+
if (t !== null && (t.type === JSONB || t.type === JSONB_ARRAY)) {
|
|
2143
|
+
throw new Unrewritable(`${b.op === "IS_LEAST" ? "LEAST" : "GREATEST"} of jsonb values is not supported by Mason (Postgres orders jsonb by its own rules)`, "ORDER BY the value … LIMIT 1 in a subquery", "jsonb-compare");
|
|
2144
|
+
}
|
|
1743
2145
|
if (t !== null)
|
|
1744
2146
|
b.args = b.args.map((a, i) => this.widen(a, t.args[i]?.type));
|
|
1745
2147
|
if (t === null || !TEXTS.has(t.type) || BYTE_COLLATIONS.has(t.inputCollation) || this.collation.kind === "bytes")
|
|
@@ -1766,9 +2168,74 @@ class Rewriter {
|
|
|
1766
2168
|
// jsonb's jsonpath operators: Postgres's answer for each row (src/duck/pg-eval.ts)
|
|
1767
2169
|
if (b.kind === "AEXPR_OP" && (name === "@?" || name === "@@") && a0 === JSONB && l !== undefined && r !== undefined)
|
|
1768
2170
|
return pgCall("bool", `a1::jsonb ${name} a2::jsonpath`, [l, r]);
|
|
2171
|
+
// x = ANY (ARRAY(SELECT …)): DuckDB binds the array's subquery as a comparison of its own and refuses the types
|
|
2172
|
+
if ((b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL") && r !== undefined && kindOf(r) === "SubLink" && bodyOf(r).subLinkType === "ARRAY_SUBLINK") {
|
|
2173
|
+
throw new Unrewritable(`${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} of ARRAY(SELECT …) is not supported by Mason`, `x ${name} ${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} (SELECT …), without the ARRAY`, "array-subquery-element");
|
|
2174
|
+
}
|
|
1769
2175
|
if ((b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL") && this.isCitext(a0)) {
|
|
1770
2176
|
throw new Unrewritable("a citext compared with ANY or ALL of an array is not supported by Mason", "x IN (…) with the values written out, or lower(x) = ANY (…)", "citext-forms");
|
|
1771
2177
|
}
|
|
2178
|
+
// A JSONB COMPARED (jsonb's =, <>, <, … and its btree's order: src/duck/jsonb-order.ts), each side as its order key
|
|
2179
|
+
// (an operand of a jsonb operator is a jsonb, a literal included: each is keyed, whatever its own analysis says)
|
|
2180
|
+
const lType = l === undefined ? null : this.typeOf(l);
|
|
2181
|
+
const comparing = ["=", "<>", "!=", "<", "<=", ">", ">="].includes(name);
|
|
2182
|
+
// rows compared member by member (`(id, j) = (1, '…')`: Postgres's per-member operators; a struct to DuckDB,
|
|
2183
|
+
// compared by its text)
|
|
2184
|
+
if ((l !== undefined && this.rowHoldsJsonb(l)) || (r !== undefined && this.rowHoldsJsonb(r))) {
|
|
2185
|
+
throw new Unrewritable("a row holding a jsonb compared with another is not supported by Mason (Postgres compares the jsonb by its own rules)", "compare the members one by one: a = x AND j = y", "jsonb-compare");
|
|
2186
|
+
}
|
|
2187
|
+
// a jsonb array's containment and overlap compare its elements by jsonb's equality, DuckDB's list operators by text
|
|
2188
|
+
if (b.kind === "AEXPR_OP" && ["@>", "<@", "&&"].includes(name) && (a0 === JSONB_ARRAY || a1 === JSONB_ARRAY)) {
|
|
2189
|
+
throw new Unrewritable(`a jsonb array's ${name} is not supported by Mason (Postgres compares its elements by jsonb's equality)`, "compare an element: a[1] = '…'::jsonb, or a::text", "jsonb-compare");
|
|
2190
|
+
}
|
|
2191
|
+
// a jsonb's containment with ANY / ALL of an array written out: each element's containment, OR'd (ALL: AND'd), as
|
|
2192
|
+
// SQL's three values are ANY's and ALL's (DuckDB has no `@> ANY`)
|
|
2193
|
+
if ((b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL") && (name === "@>" || name === "<@") && (a0 === JSONB || lType === JSONB) && l !== undefined && r !== undefined) {
|
|
2194
|
+
if (kindOf(r) !== "A_ArrayExpr" || (bodyOf(r).elements ?? []).length === 0) {
|
|
2195
|
+
throw new Unrewritable(`a jsonb's ${name} with ${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} of an array that is not written out is not supported by Mason`, `write the values out: j ${name} '…' OR j ${name} '…'`, "jsonb-compare");
|
|
2196
|
+
}
|
|
2197
|
+
const each = (bodyOf(r).elements ?? []).map((e, i) => {
|
|
2198
|
+
const x = i === 0 ? l : this.clone(l);
|
|
2199
|
+
return name === "@>" ? fn("__mason_jsonb_contains", [jsonArg(x), jsonArg(e)]) : fn("__mason_jsonb_contains", [jsonArg(e), jsonArg(x)]);
|
|
2200
|
+
});
|
|
2201
|
+
return b.kind === "AEXPR_OP_ANY" ? or(each) : and(each);
|
|
2202
|
+
}
|
|
2203
|
+
// an operator's ANY / ALL compares only for a comparison (`j @> ANY (…)` is containment, above)
|
|
2204
|
+
const compares = b.kind === "AEXPR_OP" || b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL" ? comparing : true;
|
|
2205
|
+
if (l !== undefined && r !== undefined && (a0 === JSONB || a0 === JSONB_ARRAY || lType === JSONB || lType === JSONB_ARRAY) && compares) {
|
|
2206
|
+
const what = `comparing (${b.kind === "AEXPR_OP" ? name : b.kind.replace("AEXPR_", "").replace(/_/g, " ").toLowerCase()})`;
|
|
2207
|
+
if (a0 === JSONB_ARRAY || lType === JSONB_ARRAY)
|
|
2208
|
+
this.comparable(l, what);
|
|
2209
|
+
// equality by the equality key (no collation: equalKey), an order by the collation's
|
|
2210
|
+
const equating = ["=", "<>", "!="].includes(name) || ["AEXPR_DISTINCT", "AEXPR_NOT_DISTINCT", "AEXPR_NULLIF"].includes(String(b.kind));
|
|
2211
|
+
const key = (x) => equating ? this.equalKey(x) : this.jsonbKey(x);
|
|
2212
|
+
switch (b.kind) {
|
|
2213
|
+
case "AEXPR_OP": return op(name, key(l), key(r));
|
|
2214
|
+
case "AEXPR_DISTINCT":
|
|
2215
|
+
case "AEXPR_NOT_DISTINCT": return { A_Expr: { ...b, lexpr: key(l), rexpr: key(r) } };
|
|
2216
|
+
// NULLIF(a, b): NULL where a = b, else a (the value, never its key)
|
|
2217
|
+
case "AEXPR_NULLIF": return caseWhen([[op("=", key(l), key(r)), nullConst()]], this.clone(l));
|
|
2218
|
+
case "AEXPR_IN":
|
|
2219
|
+
if (kindOf(r) === "List")
|
|
2220
|
+
return { A_Expr: { ...b, lexpr: key(l), rexpr: { List: { items: (bodyOf(r).items ?? []).map(key) } } } };
|
|
2221
|
+
break;
|
|
2222
|
+
case "AEXPR_OP_ANY":
|
|
2223
|
+
case "AEXPR_OP_ALL":
|
|
2224
|
+
if (kindOf(r) === "A_ArrayExpr" && comparing)
|
|
2225
|
+
return { A_Expr: { ...b, lexpr: key(l), rexpr: { A_ArrayExpr: { ...bodyOf(r), elements: (bodyOf(r).elements ?? []).map(key) } } } };
|
|
2226
|
+
throw new Unrewritable(`a jsonb compared with ${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} of an array that is not written out is not supported by Mason`, "write the values out: x IN ('…'::jsonb, …), or ARRAY['…'::jsonb, …]", "jsonb-compare");
|
|
2227
|
+
case "AEXPR_BETWEEN":
|
|
2228
|
+
case "AEXPR_NOT_BETWEEN": {
|
|
2229
|
+
const [lo, hi] = (bodyOf(r).items ?? []);
|
|
2230
|
+
if (lo === undefined || hi === undefined)
|
|
2231
|
+
break;
|
|
2232
|
+
const inside = and([op(">=", key(l), key(lo)), op("<=", key(this.clone(l)), key(hi))]);
|
|
2233
|
+
return b.kind === "AEXPR_NOT_BETWEEN" ? not(inside) : inside;
|
|
2234
|
+
}
|
|
2235
|
+
default: break;
|
|
2236
|
+
}
|
|
2237
|
+
throw new Unrewritable(`${what} a jsonb value is not supported by Mason (Postgres orders and equates jsonb by its own rules)`, JSONB_WAY_OUT, "jsonb-compare");
|
|
2238
|
+
}
|
|
1772
2239
|
switch (b.kind) {
|
|
1773
2240
|
case "AEXPR_LIKE":
|
|
1774
2241
|
case "AEXPR_ILIKE": {
|
|
@@ -2083,8 +2550,14 @@ class Rewriter {
|
|
|
2083
2550
|
b.args = b.args.map((a, i) => this.widen(a, t.args[i]?.type));
|
|
2084
2551
|
}
|
|
2085
2552
|
// --------------------------------------------------------------------------------------------- functions
|
|
2553
|
+
/** a window's PARTITION BY: a jsonb by its order key (jsonb's equality), a jsonb array refused */
|
|
2554
|
+
keyedPartition(w) {
|
|
2555
|
+
if (w !== undefined && Array.isArray(w.partitionClause))
|
|
2556
|
+
w.partitionClause = w.partitionClause.map((p) => isNode(p) ? this.equated(p, "PARTITION BY") : p);
|
|
2557
|
+
}
|
|
2086
2558
|
funcCall(b) {
|
|
2087
2559
|
const name = lastName(b.funcname);
|
|
2560
|
+
this.keyedPartition(b.over);
|
|
2088
2561
|
const t = this.typed({ FuncCall: b });
|
|
2089
2562
|
const args = (b.args ?? []);
|
|
2090
2563
|
const argType = (i) => t?.args[i]?.type ?? (args[i] !== undefined ? this.typeOf(args[i]) : null);
|
|
@@ -2109,8 +2582,20 @@ class Rewriter {
|
|
|
2109
2582
|
throw new Unrewritable(`${name}() of an interval is not supported by Mason`, "sum(extract(epoch FROM x)) for the seconds", "interval-aggregates");
|
|
2110
2583
|
if (name === "sum" && this.typeOf(args[0] ?? { A_Const: {} }) === FLOAT4 && argType(0) === FLOAT4)
|
|
2111
2584
|
throw new Unrewritable("sum() of a real is not supported by Mason (Postgres adds in single precision)", "sum(x::double precision)", "sum-real");
|
|
2112
|
-
|
|
2585
|
+
// count(DISTINCT x) of a column unique on the source, over a FROM that repeats none of its table's rows: no value is
|
|
2586
|
+
// there twice, so it is count(x), its FILTER kept (src/duck/distinct-key.ts, `keyCounts`)
|
|
2587
|
+
if (b.agg_distinct === true && name === "count" && typeof b.location === "number" && this.keyCounts.has(b.location))
|
|
2588
|
+
delete b.agg_distinct;
|
|
2589
|
+
// count(DISTINCT j) counts jsonb's equal values once: their keys, counted (another aggregate's DISTINCT would hand
|
|
2590
|
+
// the key itself to the aggregate)
|
|
2591
|
+
if (b.agg_distinct === true && args[0] !== undefined && name === "count" && args.length === 1 && this.typeOf(args[0]) === JSONB)
|
|
2592
|
+
args[0] = this.equalKey(args[0]);
|
|
2593
|
+
else if (b.agg_distinct === true && args[0] !== undefined)
|
|
2113
2594
|
this.comparable(args[0], `${name}(DISTINCT …) of`);
|
|
2595
|
+
// a jsonb array searched for an element: jsonb's equality, where DuckDB's list functions compare the text
|
|
2596
|
+
if (["array_position", "array_positions", "array_remove", "array_replace"].includes(name) && args[0] !== undefined && this.typeOf(args[0]) === JSONB_ARRAY) {
|
|
2597
|
+
throw new Unrewritable(`${name}() of a jsonb array is not supported by Mason (Postgres compares its elements by jsonb's equality)`, "compare an element: a[1] = '…'::jsonb", "jsonb-compare");
|
|
2598
|
+
}
|
|
2114
2599
|
// a function-style cast (`date(x)`, `text(x)`, `numeric(x)`) is the cast
|
|
2115
2600
|
if (t !== null && (t.node === "COERCEVIAIO" || t.node === "RELABELTYPE" || (t.node === "FUNCEXPR" && t.format !== null && t.format !== 0 && t.format !== 3)) && args.length === 1) {
|
|
2116
2601
|
if (t.type === TEXT || t.type === VARCHAR) {
|
|
@@ -2188,10 +2673,18 @@ class Rewriter {
|
|
|
2188
2673
|
return null;
|
|
2189
2674
|
case "date_trunc":
|
|
2190
2675
|
if (args.length === 3) {
|
|
2191
|
-
// in a zone: the
|
|
2676
|
+
// in a zone: the instant's own arithmetic where that is Postgres's answer (truncInstant), else the wall clock
|
|
2677
|
+
// there truncated, read back as an instant in that zone
|
|
2192
2678
|
const [unit, ts] = args;
|
|
2193
2679
|
const zone = this.zoneArg(args[2]);
|
|
2194
|
-
|
|
2680
|
+
const instant = this.asInstant(ts, argType(1));
|
|
2681
|
+
const fast = this.truncInstant((constString(unit) ?? "").toLowerCase(), instant, zone, constString(zone));
|
|
2682
|
+
// date_trunc is strict and reads its zone first: a zone not a constant name (a row's, a NULL) is read here
|
|
2683
|
+
// (`__mason_zone`, which refuses a name Postgres would) and a NULL one is NULL, whatever the part — a second
|
|
2684
|
+
// or less never names the zone, and a minute's or an hour's ±infinity is the instant's own
|
|
2685
|
+
if (fast !== null)
|
|
2686
|
+
return constString(zone) !== null ? fast : caseWhen([[isNull(zone), nullConst()]], fast);
|
|
2687
|
+
return fn("timezone", [zone, this.truncWall(unit, fn("timezone", [this.clone(zone), instant]), TIMESTAMP)]);
|
|
2195
2688
|
}
|
|
2196
2689
|
if (args.length !== 2)
|
|
2197
2690
|
return null;
|
|
@@ -2203,20 +2696,25 @@ class Rewriter {
|
|
|
2203
2696
|
if (argType(1) === INTERVAL && unit === "week")
|
|
2204
2697
|
throw new Unrewritable(`unit "week" not supported for type interval`, "", { postgres: "0A000" });
|
|
2205
2698
|
const x = argType(1) === TIMESTAMPTZ && own === DATE ? cast(args[1], ["timestamptz"]) : args[1];
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2699
|
+
const wall = (v) => (unit === "century" || unit === "millennium" || unit === "decade") && argType(1) !== INTERVAL
|
|
2700
|
+
? this.truncWall(args[0], v, argType(1) === TIMESTAMPTZ ? TIMESTAMPTZ : TIMESTAMP)
|
|
2701
|
+
: v === args[1] ? null : fn("date_trunc", [args[0], v]);
|
|
2702
|
+
// a timestamptz in the session's zone: the instant's own arithmetic where that is Postgres's answer. The
|
|
2703
|
+
// zone is the one the statement EXECUTES in, never the one it was planned in (a prepared statement is
|
|
2704
|
+
// planned at its Parse and run after any later SET TimeZone): `current_setting('TimeZone')`, which DuckDB
|
|
2705
|
+
// folds to a constant when it plans the statement it runs, so the CASE costs no row anything
|
|
2706
|
+
if (argType(1) === TIMESTAMPTZ) {
|
|
2707
|
+
const session = () => fn("current_setting", [strConst("TimeZone")]);
|
|
2708
|
+
const utc = this.truncInstant(unit, this.clone(x), session(), "UTC");
|
|
2709
|
+
if (utc !== null && ["microseconds", "milliseconds", "second"].includes(unit))
|
|
2710
|
+
return utc;
|
|
2711
|
+
if (utc !== null) {
|
|
2712
|
+
const inUtc = or(UTC_NAMES.map((z) => op("=", session(), strConst(z))));
|
|
2713
|
+
const elsewhere = this.truncInstant(unit, x, session(), null) ?? wall(x) ?? fn("date_trunc", [args[0], x]);
|
|
2714
|
+
return caseWhen([[inUtc, utc]], elsewhere);
|
|
2715
|
+
}
|
|
2218
2716
|
}
|
|
2219
|
-
return
|
|
2717
|
+
return wall(x);
|
|
2220
2718
|
}
|
|
2221
2719
|
case "make_interval": return makeInterval(args);
|
|
2222
2720
|
// isfinite of a time or an interval: DuckDB has none, and neither is ever infinite here
|
|
@@ -2355,7 +2853,7 @@ class Rewriter {
|
|
|
2355
2853
|
case "json_object_keys":
|
|
2356
2854
|
if (this.inFrom.has(b))
|
|
2357
2855
|
return null;
|
|
2358
|
-
return fn("unnest", [fn("__mason_json_keys", [args[0], boolConst(name.startsWith("jsonb"))])]);
|
|
2856
|
+
return fn("unnest", [fn("__mason_json_keys", [args[0], boolConst(name.startsWith("jsonb")), boolConst(false)])]);
|
|
2359
2857
|
case "jsonb_array_elements":
|
|
2360
2858
|
case "json_array_elements":
|
|
2361
2859
|
case "jsonb_array_elements_text":
|
|
@@ -2424,6 +2922,8 @@ class Rewriter {
|
|
|
2424
2922
|
case "family": return argType(0) === INET && args.length === 1
|
|
2425
2923
|
? caseWhen([[{ NullTest: { arg: this.cp(args[0]), nulltesttype: "IS_NULL" } }, nullConst()], [fn("contains", [args[0], strConst(":")]), intConst(6)]], intConst(4)) : null;
|
|
2426
2924
|
case "abbrev": return argType(0) === INET && args.length === 1 ? inetOut(args[0]) : null;
|
|
2925
|
+
// the session's setting, never DuckDB's own (`settingRead`)
|
|
2926
|
+
case "current_setting": return args.length === 1 || args.length === 2 ? settingRead(this.cp, args) : null;
|
|
2427
2927
|
// the transaction's start, as of the execution (sqlValueFunction says why)
|
|
2428
2928
|
case "now":
|
|
2429
2929
|
case "transaction_timestamp": return args.length === 0 ? this.now("exec") : null;
|
|
@@ -2493,6 +2993,72 @@ class Rewriter {
|
|
|
2493
2993
|
const own = this.typeOf(n);
|
|
2494
2994
|
return wanted === TIMESTAMPTZ && own !== null && own !== TIMESTAMPTZ && own !== UNKNOWN ? cast(n, ["timestamptz"]) : n;
|
|
2495
2995
|
}
|
|
2996
|
+
/**
|
|
2997
|
+
* A TIMESTAMPTZ TRUNCATED AS THE INSTANT'S OWN ARITHMETIC (docs/DESIGN.md "date_trunc of a timestamptz"), where
|
|
2998
|
+
* that is exactly Postgres's `timestamptz_trunc_internal` and cheaper than DuckDB's: DuckDB's `date_trunc(part,
|
|
2999
|
+
* TIMESTAMPTZ)` is ICU's calendar, ~45 ns a row a thread even in UTC (and its sub-day parts in another zone re-read the
|
|
3000
|
+
* wall clock, so an instant in a repeated hour lands on the other side of the transition, where Postgres keeps its
|
|
3001
|
+
* offset), while the instant's microseconds (`epoch_us`, `make_timestamptz`) are ~7. Postgres truncates the wall clock
|
|
3002
|
+
* in `zone` and, for a day or longer, reads the truncated wall clock back in that zone (`redotz`), else keeps the
|
|
3003
|
+
* instant's own offset. So, the instant's microseconds `u`, floored by integer arithmetic (`%` is truncating, hence
|
|
3004
|
+
* `((u % k) + k) % k`):
|
|
3005
|
+
* - microseconds: the instant itself;
|
|
3006
|
+
* - milliseconds, second: `u` floored, in every zone (an offset is whole seconds);
|
|
3007
|
+
* - minute, hour: in UTC `u` floored; elsewhere `u` less the wall clock's remainder (one zone conversion, the
|
|
3008
|
+
* offset kept);
|
|
3009
|
+
* - day, week (Monday's, ISO: 1970-01-01 was a Thursday), month and up: in UTC only — `u` floored by a day or a
|
|
3010
|
+
* week, or the naive calendar (`make_timestamp(u)`, no zone) truncated as a timestamp is (`truncWall`); in
|
|
3011
|
+
* another zone the wall clock read back is a zone lookup DuckDB's own call makes as cheaply, so null (the caller's
|
|
3012
|
+
* path).
|
|
3013
|
+
* `zoneName` is the zone as a constant (UTC and its aliases: `isUtc`), null for a zone a row holds. ±infinity is
|
|
3014
|
+
* itself (Postgres 14+) and NULL is NULL: the CASE's ELSE. null where the unit is not a constant this handles.
|
|
3015
|
+
*/
|
|
3016
|
+
truncInstant(unit, value, zone, zoneName) {
|
|
3017
|
+
// a literal Postgres read as a timestamptz is one to DuckDB too (in the session's zone, as Postgres reads it)
|
|
3018
|
+
const x = this.typeOf(value) === UNKNOWN ? cast(value, ["timestamptz"]) : value;
|
|
3019
|
+
const utc = zoneName !== null && isUtc(zoneName);
|
|
3020
|
+
const DAY = 86400000000n;
|
|
3021
|
+
const k = { milliseconds: 1000n, second: 1000000n, minute: 60000000n, hour: 3600000000n, day: DAY }[unit];
|
|
3022
|
+
const big = (n) => n <= 2147483647n ? intConst(Number(n)) : numConst(String(n));
|
|
3023
|
+
const u = () => fn("epoch_us", [this.clone(x)]);
|
|
3024
|
+
const floorMod = (v, by, plus = 0n) => op("%", op("+", op("%", v(), big(by)), big(by + plus)), big(by));
|
|
3025
|
+
let micros;
|
|
3026
|
+
if (unit === "microseconds")
|
|
3027
|
+
return x;
|
|
3028
|
+
if (k !== undefined && (utc || unit === "milliseconds" || unit === "second"))
|
|
3029
|
+
micros = op("-", u(), floorMod(u, k));
|
|
3030
|
+
else if (k !== undefined && unit !== "day")
|
|
3031
|
+
micros = op("-", u(), floorMod(() => fn("epoch_us", [fn("timezone", [this.clone(zone), this.clone(x)])]), k));
|
|
3032
|
+
else if (!utc)
|
|
3033
|
+
return null;
|
|
3034
|
+
// Monday's: u - ((u + 3 days) mod 7 days), as ((u % 7d) + 10d) % 7d, which no instant overflows
|
|
3035
|
+
else if (unit === "week")
|
|
3036
|
+
micros = op("-", u(), floorMod(u, 7n * DAY, 3n * DAY));
|
|
3037
|
+
else if (["month", "quarter", "year", "decade", "century", "millennium"].includes(unit))
|
|
3038
|
+
micros = fn("epoch_us", [this.truncWall(strConst(unit), fn("make_timestamp", [u()]), TIMESTAMP)]);
|
|
3039
|
+
else
|
|
3040
|
+
return null;
|
|
3041
|
+
return caseWhen([[fn("isfinite", [this.clone(x)]), fn("make_timestamptz", [micros])]], x);
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* `date_trunc(unit, x)` of a wall clock (a timestamp, or a timestamptz read in the session's zone: `type`), DuckDB's
|
|
3045
|
+
* except where Postgres counts otherwise: its centuries and millennia begin in a year 1 (2001-01-01), DuckDB's in a
|
|
3046
|
+
* year 0 (DT_CENTURY), and a decade before year 1 is counted back as timestamp_trunc's DTK_DECADE counts it.
|
|
3047
|
+
*/
|
|
3048
|
+
truncWall(unitNode, x, type) {
|
|
3049
|
+
const unit = (constString(unitNode) ?? "").toLowerCase();
|
|
3050
|
+
if (unit !== "century" && unit !== "millennium" && unit !== "decade")
|
|
3051
|
+
return fn("date_trunc", [unitNode, x]);
|
|
3052
|
+
const y = fn("year", [this.clone(x)]);
|
|
3053
|
+
const first = unit === "decade"
|
|
3054
|
+
? caseWhen([[op(">", y, intConst(0)), op("*", op("//", this.clone(y), intConst(10)), intConst(10))]], op("*", op("-", intConst(0), op("//", op("-", intConst(8), op("-", this.clone(y), intConst(1))), intConst(10))), intConst(10)))
|
|
3055
|
+
: (() => {
|
|
3056
|
+
const n = unit === "century" ? 100 : 1000;
|
|
3057
|
+
return caseWhen([[op(">", y, intConst(0)), op("-", op("*", op("//", op("+", this.clone(y), intConst(n - 1)), intConst(n)), intConst(n)), intConst(n - 1))]], op("+", op("*", op("-", intConst(0), op("//", op("-", intConst(n - 1), op("-", this.clone(y), intConst(1))), intConst(n))), intConst(n)), intConst(1)));
|
|
3058
|
+
})();
|
|
3059
|
+
const truncated = op("+", fn("date_trunc", [strConst("year"), this.clone(x)]), fn("to_years", [cast(op("-", first, this.clone(y)), ["integer"])]));
|
|
3060
|
+
return caseWhen([[fn("isfinite", [this.clone(x)]), truncated]], type === TIMESTAMPTZ ? cast(x, ["timestamptz"]) : cast(x, ["timestamp"]));
|
|
3061
|
+
}
|
|
2496
3062
|
textMinMax(name, b, args) {
|
|
2497
3063
|
const arg = args[0];
|
|
2498
3064
|
// a window's too (`max(t) OVER (…)`): arg_min / arg_max are DuckDB window aggregates as well
|
|
@@ -2516,6 +3082,10 @@ class Rewriter {
|
|
|
2516
3082
|
withinGroup(name, b) {
|
|
2517
3083
|
const order = (b.agg_order ?? []);
|
|
2518
3084
|
const key = order[0] !== undefined ? bodyOf(order[0]).node : undefined;
|
|
3085
|
+
// a jsonb's position in its order, or its most frequent value: DuckDB would pick by its text
|
|
3086
|
+
if (key !== undefined && (this.typeOf(key) === JSONB || this.typeOf(key) === JSONB_ARRAY)) {
|
|
3087
|
+
throw new Unrewritable(`${name}() WITHIN GROUP (ORDER BY a jsonb) is not supported by Mason (Postgres orders jsonb by its own rules)`, "ORDER BY the value … LIMIT 1 OFFSET n in a subquery", "jsonb-compare");
|
|
3088
|
+
}
|
|
2519
3089
|
const c = key === undefined ? null : this.textCollation(key);
|
|
2520
3090
|
if (c === null || c.kind === "bytes")
|
|
2521
3091
|
return null;
|
|
@@ -2921,6 +3491,395 @@ class Rewriter {
|
|
|
2921
3491
|
return { base: deeper === null ? b.lexpr : deeper.base, steps: [...(deeper === null ? [] : deeper.steps), ...JSON.parse(own)] };
|
|
2922
3492
|
}
|
|
2923
3493
|
// --------------------------------------------------------------------------------------------- FROM
|
|
3494
|
+
/**
|
|
3495
|
+
* WHERE THE ORDER OF A JOIN'S ROWS DECIDES THE ANSWER (leftLateral), computed for the whole statement before the walk
|
|
3496
|
+
* (`rewrite`): libpg-query writes a SELECT's keys in its struct's order (fromClause … limitCount, withClause, larg,
|
|
3497
|
+
* rarg) and the walk follows it, so a SELECT's CTEs and arms are met after its FROM, and a CTE another CTE reads is
|
|
3498
|
+
* met before the one that reads it. So every SELECT of the statement is collected with the CTEs it sees (its own
|
|
3499
|
+
* WITH's and those around it, the nearest first), and `limitBounds` runs over them all until no more is bounded.
|
|
3500
|
+
*
|
|
3501
|
+
* `wire`: the statement's own rows are bounded by a row count the Execute sets on the wire (JDBC's setMaxRows or
|
|
3502
|
+
* fetch size: the client reads the first rows and may read no more), which the rewrite never sees in the SQL — its
|
|
3503
|
+
* outermost SELECT is bounded as a SELECT a LIMIT reads is (limitBounds: no aggregate, GROUP BY, DISTINCT, window or
|
|
3504
|
+
* set operation but UNION ALL between, and under an ORDER BY only a LIMIT of its own, which a row count on the wire
|
|
3505
|
+
* is not: DuckDB sorts every row first whichever plan it is given). Unless `wire`, the bounds a wire's row count
|
|
3506
|
+
* would add are computed too (`boundedOnWire`), so the plan says whether such an Execute plans it again.
|
|
3507
|
+
*/
|
|
3508
|
+
markBounds(root, wire = false) {
|
|
3509
|
+
this.bounded = this.boundsOf(root, wire);
|
|
3510
|
+
this.boundedOnWire = wire ? null : this.boundsOf(root, true);
|
|
3511
|
+
}
|
|
3512
|
+
boundsOf(root, wire) {
|
|
3513
|
+
const selects = [];
|
|
3514
|
+
const select = (b, outer) => {
|
|
3515
|
+
const scope = new Map(outer);
|
|
3516
|
+
for (const c of (b.withClause?.ctes ?? [])) {
|
|
3517
|
+
const cb = isNode(c) ? bodyOf(c) : null;
|
|
3518
|
+
if (cb !== null && typeof cb.ctename === "string" && isNode(cb.ctequery) && kindOf(cb.ctequery) === "SelectStmt")
|
|
3519
|
+
scope.set(cb.ctename, bodyOf(cb.ctequery));
|
|
3520
|
+
}
|
|
3521
|
+
selects.push({ b, scope });
|
|
3522
|
+
for (const [k, x] of Object.entries(b)) {
|
|
3523
|
+
const arm = armOf(k, x);
|
|
3524
|
+
if (arm !== null)
|
|
3525
|
+
select(bodyOf(arm), scope);
|
|
3526
|
+
else
|
|
3527
|
+
visit(x, scope);
|
|
3528
|
+
}
|
|
3529
|
+
};
|
|
3530
|
+
const visit = (v, scope) => {
|
|
3531
|
+
if (Array.isArray(v)) {
|
|
3532
|
+
for (const x of v)
|
|
3533
|
+
visit(x, scope);
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
if (v === null || typeof v !== "object")
|
|
3537
|
+
return;
|
|
3538
|
+
if (isNode(v) && kindOf(v) === "SelectStmt") {
|
|
3539
|
+
select(bodyOf(v), scope);
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
3542
|
+
for (const x of Object.values(isNode(v) ? bodyOf(v) : v))
|
|
3543
|
+
visit(x, scope);
|
|
3544
|
+
};
|
|
3545
|
+
visit(root, new Map());
|
|
3546
|
+
const set = new WeakSet();
|
|
3547
|
+
let count = 0;
|
|
3548
|
+
const bound = (b) => { if (!set.has(b)) {
|
|
3549
|
+
set.add(b);
|
|
3550
|
+
count++;
|
|
3551
|
+
} };
|
|
3552
|
+
if (wire && isNode(root) && kindOf(root) === "SelectStmt")
|
|
3553
|
+
bound(bodyOf(root));
|
|
3554
|
+
let before;
|
|
3555
|
+
do {
|
|
3556
|
+
before = count;
|
|
3557
|
+
for (const s of selects)
|
|
3558
|
+
this.limitBounds(s.b, s.scope, set, bound);
|
|
3559
|
+
} while (count !== before);
|
|
3560
|
+
return set;
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* One SELECT's part of markBounds. Its FROM's order decides the answer where:
|
|
3564
|
+
* - a LIMIT or an OFFSET keeps the first of its rows — its own, or one around it whose rows are its rows in their
|
|
3565
|
+
* order (`bounded`: the SELECT reads it as a derived table or a CTE, or as an arm of its UNION ALL) — with no
|
|
3566
|
+
* aggregate, GROUP BY, DISTINCT, window or other set operation between, each of which reads every row first and
|
|
3567
|
+
* hands back an order of its own. Under an ORDER BY only a LIMIT of its own bounds (never WITH TIES: Mason ranks
|
|
3568
|
+
* every row for it, src/duck/rewrite.ts withTies), and only when its first key, once the nulls idiom is folded
|
|
3569
|
+
* (nullsKeyFolded), is a column of a table sorted bare (sortsByTableColumn): DuckDB's top N pushes its bound into
|
|
3570
|
+
* that table's scan as a filter, so the rows reaching the lateral are few (measured, docs/DESIGN.md "A LEFT lateral
|
|
3571
|
+
* of a list": 0.1–6 s streamed against 0.4–106 s for the delimiter join over 20 M rows); any other key — the
|
|
3572
|
+
* lateral's own column, an expression, a text under a collation, a citext, a char(n), a jsonb, each sorted by an
|
|
3573
|
+
* expression — is a top N over every row, each row's function calls paid in full;
|
|
3574
|
+
* - an aggregate that reads its rows in order (string_agg, array_agg, json_agg …) has no ORDER BY of its own, or a
|
|
3575
|
+
* window function that reads its frame in order (row_number, lag, first_value …, or such an aggregate) has a
|
|
3576
|
+
* window with no ORDER BY: Postgres hands each base row's members to it in their order (`readsInOrder`).
|
|
3577
|
+
* Its joins, and the derived tables and CTEs its FROM reads, are bounded. An OFFSET alone is bounded for the
|
|
3578
|
+
* answer's sake (Postgres's rows after it) and streams every row it does not skip; a CTE read twice or MATERIALIZED
|
|
3579
|
+
* is computed whole, a call a row. Bounding too much costs time, never an answer (the streamed lateral is Postgres's
|
|
3580
|
+
* rows in Postgres's order); bounding too little keeps DuckDB's delimiter join, whose rows come in an order of its own.
|
|
3581
|
+
*/
|
|
3582
|
+
limitBounds(b, scope, bounded, bound) {
|
|
3583
|
+
const keeps = (n) => isNode(n) && !(kindOf(n) === "A_Const" && bodyOf(n).isnull === true);
|
|
3584
|
+
const sort = (b.sortClause ?? []);
|
|
3585
|
+
const inOrder = this.readsInOrder(b);
|
|
3586
|
+
if (!inOrder) {
|
|
3587
|
+
if (!keeps(b.limitCount) && !keeps(b.limitOffset) && !bounded.has(b))
|
|
3588
|
+
return;
|
|
3589
|
+
if (b.limitOption === "LIMIT_OPTION_WITH_TIES")
|
|
3590
|
+
return;
|
|
3591
|
+
// a UNION ALL's arms, each read in turn (a set operation's arm is a SELECT body, unwrapped: walkBody)
|
|
3592
|
+
if (b.op !== undefined && b.op !== "SETOP_NONE") {
|
|
3593
|
+
if (b.op === "SETOP_UNION" && b.all === true && sort.length === 0)
|
|
3594
|
+
for (const arm of [b.larg, b.rarg])
|
|
3595
|
+
if (arm !== null && typeof arm === "object" && !isNode(arm))
|
|
3596
|
+
bound(arm);
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
if (sort.length > 0) {
|
|
3600
|
+
const first = nullsKeyFolded(sort, b)[0];
|
|
3601
|
+
if (!keeps(b.limitCount) || first === undefined || kindOf(first) !== "SortBy" || !this.sortsByTableColumn(bodyOf(first).node, b, scope))
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
if ((Array.isArray(b.groupClause) && b.groupClause.length > 0) || b.havingClause !== undefined || b.distinctClause !== undefined)
|
|
3605
|
+
return;
|
|
3606
|
+
if ((Array.isArray(b.windowClause) && b.windowClause.length > 0) || (b.targetList ?? []).some((t) => this.hasAggregate(t) || hasWindow(t)))
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
else if (b.op !== undefined && b.op !== "SETOP_NONE")
|
|
3610
|
+
return;
|
|
3611
|
+
const visit = (x) => {
|
|
3612
|
+
if (!isNode(x))
|
|
3613
|
+
return;
|
|
3614
|
+
const xb = bodyOf(x);
|
|
3615
|
+
if (kindOf(x) === "JoinExpr") {
|
|
3616
|
+
bound(xb);
|
|
3617
|
+
visit(xb.larg);
|
|
3618
|
+
visit(xb.rarg);
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
if (kindOf(x) === "RangeSubselect" && isNode(xb.subquery)) {
|
|
3622
|
+
bound(bodyOf(xb.subquery));
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
if (kindOf(x) === "RangeVar" && xb.schemaname === undefined) {
|
|
3626
|
+
const cte = scope.get(String(xb.relname));
|
|
3627
|
+
if (cte !== undefined)
|
|
3628
|
+
bound(cte);
|
|
3629
|
+
}
|
|
3630
|
+
};
|
|
3631
|
+
for (const x of (b.fromClause ?? []))
|
|
3632
|
+
visit(x);
|
|
3633
|
+
}
|
|
3634
|
+
/** whether a SELECT's aggregate or window function reads its rows in the order they come (limitBounds): an
|
|
3635
|
+
* order-sensitive aggregate with no ORDER BY of its own (nor DISTINCT, whose values Postgres sorts), or a window
|
|
3636
|
+
* function that reads its frame in order over a window with no ORDER BY; not inside a subquery */
|
|
3637
|
+
readsInOrder(b) {
|
|
3638
|
+
const windows = new Map((b.windowClause ?? []).filter(isNode).map((w) => [String(bodyOf(w).name ?? ""), bodyOf(w)]));
|
|
3639
|
+
const ordered = (w) => {
|
|
3640
|
+
if (w === undefined)
|
|
3641
|
+
return false;
|
|
3642
|
+
if (Array.isArray(w.orderClause) && w.orderClause.length > 0)
|
|
3643
|
+
return true;
|
|
3644
|
+
return typeof w.refname === "string" && ordered(windows.get(w.refname));
|
|
3645
|
+
};
|
|
3646
|
+
const reads = (v) => {
|
|
3647
|
+
if (Array.isArray(v))
|
|
3648
|
+
return v.some(reads);
|
|
3649
|
+
if (v === null || typeof v !== "object")
|
|
3650
|
+
return false;
|
|
3651
|
+
if (!isNode(v))
|
|
3652
|
+
return Object.values(v).some(reads);
|
|
3653
|
+
if (kindOf(v) === "SubLink")
|
|
3654
|
+
return false;
|
|
3655
|
+
if (kindOf(v) === "FuncCall") {
|
|
3656
|
+
const c = bodyOf(v), name = lastName(c.funcname);
|
|
3657
|
+
const aggregate = IN_ORDER_AGGREGATES.has(name) && c.agg_order === undefined && c.agg_distinct !== true;
|
|
3658
|
+
if (c.over === undefined ? aggregate : (aggregate || IN_ORDER_WINDOWS.has(name)) && !ordered(c.over))
|
|
3659
|
+
return true;
|
|
3660
|
+
}
|
|
3661
|
+
return Object.values(bodyOf(v)).some(reads);
|
|
3662
|
+
};
|
|
3663
|
+
return reads(b.targetList) || reads(b.havingClause);
|
|
3664
|
+
}
|
|
3665
|
+
/** whether an ORDER BY key is a column of a table sorted as it is stored (limitBounds): an ordinal or an output
|
|
3666
|
+
* name is its target's expression, and a derived table's or a CTE's column is followed to what it selects */
|
|
3667
|
+
sortsByTableColumn(key, b, scope) {
|
|
3668
|
+
const targets = (b.targetList ?? []);
|
|
3669
|
+
const n = constInt(key);
|
|
3670
|
+
if (n !== null)
|
|
3671
|
+
return n >= 1 && n <= targets.length && this.tableColumn(bodyOf(targets[n - 1]).val, b, scope, 0);
|
|
3672
|
+
const f = kindOf(key) === "ColumnRef" ? names(bodyOf(key).fields) : [];
|
|
3673
|
+
const named = f.length === 1 ? targets.filter((t) => bodyOf(t).name === f[0]) : [];
|
|
3674
|
+
return this.tableColumn(named.length === 1 ? bodyOf(named[0]).val : key, b, scope, 0);
|
|
3675
|
+
}
|
|
3676
|
+
/** a column of a table, or of a derived table or CTE that selects one, sorted bare: no collation's keys, citext's
|
|
3677
|
+
* lower(), char(n)'s rtrim() or jsonb's order key (sortBy), each an expression no top N filter reaches the scan by.
|
|
3678
|
+
* (A CTE is looked up among those the SELECT sees; a derived table's own WITH is not followed through its `*`.) */
|
|
3679
|
+
tableColumn(e, select, scope, depth) {
|
|
3680
|
+
if (!isNode(e) || kindOf(e) !== "ColumnRef" || depth > 8)
|
|
3681
|
+
return false;
|
|
3682
|
+
const t = this.typed(e);
|
|
3683
|
+
if (t !== null) {
|
|
3684
|
+
if (t.node !== "VAR" || t.functionColumn !== undefined || (t.levelsUp ?? 0) !== 0)
|
|
3685
|
+
return false;
|
|
3686
|
+
if (t.type === JSONB || t.type === BPCHAR || t.type === INET || this.isCitext(t.type))
|
|
3687
|
+
return false;
|
|
3688
|
+
const c = this.textCollation(e);
|
|
3689
|
+
if (c !== null && c.kind !== "bytes")
|
|
3690
|
+
return false;
|
|
3691
|
+
if (t.relation !== undefined)
|
|
3692
|
+
return true;
|
|
3693
|
+
}
|
|
3694
|
+
// a derived table's or a CTE's column: what it selects under that name, or a column of the one table its * reads
|
|
3695
|
+
const f = names(bodyOf(e).fields);
|
|
3696
|
+
const col = f.at(-1), alias = f.length === 2 ? f[0] : undefined;
|
|
3697
|
+
const sources = [];
|
|
3698
|
+
const from = (x) => {
|
|
3699
|
+
if (!isNode(x))
|
|
3700
|
+
return;
|
|
3701
|
+
const xb = bodyOf(x);
|
|
3702
|
+
if (kindOf(x) === "JoinExpr") {
|
|
3703
|
+
from(xb.larg);
|
|
3704
|
+
from(xb.rarg);
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
if (kindOf(x) === "RangeSubselect" && typeof xb.alias?.aliasname === "string" && isNode(xb.subquery) && xb.alias?.colnames === undefined)
|
|
3708
|
+
sources.push({ alias: xb.alias.aliasname, query: bodyOf(xb.subquery) });
|
|
3709
|
+
if (kindOf(x) === "RangeVar" && xb.schemaname === undefined && xb.alias?.colnames === undefined) {
|
|
3710
|
+
const cte = scope.get(String(xb.relname));
|
|
3711
|
+
if (cte !== undefined)
|
|
3712
|
+
sources.push({ alias: xb.alias?.aliasname ?? String(xb.relname), query: cte });
|
|
3713
|
+
}
|
|
3714
|
+
};
|
|
3715
|
+
for (const x of (select.fromClause ?? []))
|
|
3716
|
+
from(x);
|
|
3717
|
+
const matching = sources.filter((s) => alias === undefined || s.alias === alias);
|
|
3718
|
+
if (col === undefined || matching.length !== 1)
|
|
3719
|
+
return false;
|
|
3720
|
+
const q = matching[0].query;
|
|
3721
|
+
if (q.op !== undefined && q.op !== "SETOP_NONE")
|
|
3722
|
+
return false;
|
|
3723
|
+
const qt = (q.targetList ?? []);
|
|
3724
|
+
const hits = qt.filter((x) => isNode(bodyOf(x).val) && !hasStar(bodyOf(x).val) && (bodyOf(x).name ?? figureColname(bodyOf(x).val)) === col);
|
|
3725
|
+
if (hits.length === 1)
|
|
3726
|
+
return this.tableColumn(bodyOf(hits[0]).val, q, scope, depth + 1);
|
|
3727
|
+
const qf = (q.fromClause ?? []);
|
|
3728
|
+
return t !== null && hits.length === 0 && qt.some((x) => hasStar(bodyOf(x).val)) && qf.length === 1 && kindOf(qf[0]) === "RangeVar" && !scope.has(String(bodyOf(qf[0]).relname));
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* A LEFT JOIN LATERAL OF A LIST'S ROWS ON TRUE (docs/DESIGN.md "A LEFT lateral of a list"): `FROM base LEFT JOIN
|
|
3732
|
+
* json_each(base.doc) d ON TRUE` is every base row with each of its members, or once with NULLs where it has none.
|
|
3733
|
+
* DuckDB plans a LEFT lateral as a delimiter join — the whole base grouped, the function called for every distinct
|
|
3734
|
+
* argument, a hash join back — so a LIMIT over it reads every row (a BI tool's "expand the JSON" preview: 37.9 s over
|
|
3735
|
+
* 22 M rows), and the join hands its rows back in an order of its own, so the LIMIT kept OTHER rows than Postgres's
|
|
3736
|
+
* (Postgres walks the base and each row's members in order). Written as an INNER lateral over a list that is never
|
|
3737
|
+
* empty — the members, or one NULL (`orNull`) — it is the same rows, and DuckDB runs it as a streaming UNNEST: in the
|
|
3738
|
+
* base's order, the members in theirs, stopping at the LIMIT. But it calls the function once per ROW where the
|
|
3739
|
+
* delimiter join calls it once per DISTINCT argument, so where every row is read anyway — a count, a GROUP BY, a
|
|
3740
|
+
* DISTINCT, a window, a whole result, a sort by the lateral's own column — it is the slower plan (a count over 22 M
|
|
3741
|
+
* rows of few distinct documents: 79 s joined, over 200 s streamed; 2.2 s against 62.5 s over 20 M rows of 2,000).
|
|
3742
|
+
* So it is written only where the order of the join's rows decides the answer — a LIMIT keeps the first of them, or
|
|
3743
|
+
* an aggregate or a window reads them in order (`bounded`: markBounds, limitBounds) — and only for a
|
|
3744
|
+
* join that is LEFT, ON a literal TRUE, its right side one function of a list Mason writes (the json rows, an array's
|
|
3745
|
+
* unnest: a column's, or an expression's, computed once), without ORDINALITY. A RIGHT or FULL join, another
|
|
3746
|
+
* condition, or another function keeps DuckDB's own plan. Such a join a row count on the wire alone would bound
|
|
3747
|
+
* (`boundedOnWire`) is left DuckDB's and said (`unboundedLateral`): the listener plans the statement again, bounded,
|
|
3748
|
+
* for an Execute that sets one.
|
|
3749
|
+
*/
|
|
3750
|
+
leftLateral(b) {
|
|
3751
|
+
const bounded = this.bounded.has(b);
|
|
3752
|
+
if (!bounded && this.boundedOnWire?.has(b) !== true)
|
|
3753
|
+
return null;
|
|
3754
|
+
const quals = b.quals;
|
|
3755
|
+
if (b.jointype !== "JOIN_LEFT" || b.isNatural === true || b.usingClause !== undefined || quals === undefined || kindOf(quals) !== "A_Const" || bodyOf(quals).boolval?.boolval !== true)
|
|
3756
|
+
return null;
|
|
3757
|
+
const rarg = b.rarg;
|
|
3758
|
+
if (rarg === undefined || kindOf(rarg) !== "RangeFunction")
|
|
3759
|
+
return null;
|
|
3760
|
+
const r = bodyOf(rarg);
|
|
3761
|
+
const items = (r.functions ?? []);
|
|
3762
|
+
const calls = items.length === 1 ? (bodyOf(items[0]).items ?? []) : [];
|
|
3763
|
+
const call = calls[0];
|
|
3764
|
+
if (r.ordinality === true || r.is_rowsfrom === true || r.coldeflist !== undefined || call === undefined || kindOf(call) !== "FuncCall")
|
|
3765
|
+
return null;
|
|
3766
|
+
const c = bodyOf(call);
|
|
3767
|
+
const args = (c.args ?? []);
|
|
3768
|
+
const name = lastName(c.funcname);
|
|
3769
|
+
if (args.length !== 1 || c.agg_star === true || c.func_variadic === true || kindOf(args[0]) === "NamedArgExpr")
|
|
3770
|
+
return null;
|
|
3771
|
+
if (!LIST_ROWS.has(name) && name !== "unnest")
|
|
3772
|
+
return null;
|
|
3773
|
+
if (!bounded) {
|
|
3774
|
+
this.unboundedLateral = true;
|
|
3775
|
+
return null;
|
|
3776
|
+
}
|
|
3777
|
+
this.nonEmpty.add(r);
|
|
3778
|
+
const { quals: _true, ...rest } = b;
|
|
3779
|
+
// DuckDB 2.0 streams the INNER lateral only where its list reads ONE column of the row, or an array column
|
|
3780
|
+
// (measured, docs/DESIGN.md "A LEFT lateral of a list"): one that reads two or more is its delimiter join again,
|
|
3781
|
+
// the rows in an order of its own. Such an argument is computed by a step of its own before the lateral, `CROSS
|
|
3782
|
+
// JOIN LATERAL (SELECT <arg> AS __mason_arg<n>) AS __mason_arg<n>` (a name no column of the query has) — a
|
|
3783
|
+
// projection under the same UNNEST — and the lateral reads the step's one column; so is an unnest's expression of
|
|
3784
|
+
// any columns, whose never-empty CASE then names the step's column twice, at no cost but the expression's. Not where the argument reads an array (DuckDB plans that
|
|
3785
|
+
// step as a delimiter join, and the lateral streams without it: an unnest's expression is computed once inside,
|
|
3786
|
+
// rangeFunction's list_transform), nor where a `*` or a join's name reads every column of the join (the step's
|
|
3787
|
+
// would be one of them), nor for a subquery (a delimiter join in every form): `limit-lateral-argument`, decided
|
|
3788
|
+
const arg = args[0];
|
|
3789
|
+
if (kindOf(arg) === "ColumnRef" || this.readWhole.has(b))
|
|
3790
|
+
return { JoinExpr: { ...rest, jointype: "JOIN_INNER" } };
|
|
3791
|
+
const reads = this.rowReads(arg);
|
|
3792
|
+
if (reads === null || (reads < 2 && name !== "unnest"))
|
|
3793
|
+
return { JoinExpr: { ...rest, jointype: "JOIN_INNER" } };
|
|
3794
|
+
const alias = `__mason_arg${++this.steps}`;
|
|
3795
|
+
const sub = { targetList: [{ ResTarget: { name: alias, val: arg } }], op: "SETOP_NONE" };
|
|
3796
|
+
this.done.add(sub);
|
|
3797
|
+
this.stepArg.set(r, { ColumnRef: { fields: [S(alias), S(alias)] } });
|
|
3798
|
+
const step = { RangeSubselect: { lateral: true, subquery: { SelectStmt: sub }, alias: { aliasname: alias } } };
|
|
3799
|
+
return { JoinExpr: { ...rest, jointype: "JOIN_INNER", larg: { JoinExpr: { jointype: "JOIN_INNER", larg: b.larg, rarg: step } } } };
|
|
3800
|
+
}
|
|
3801
|
+
/** how many columns of the row an argument reads (each named once), or null where it reads an array, a whole row,
|
|
3802
|
+
* a column whose type Postgres did not say, or holds a subquery: a step before the lateral is no help there
|
|
3803
|
+
* (leftLateral) */
|
|
3804
|
+
rowReads(arg) {
|
|
3805
|
+
const seen = new Set();
|
|
3806
|
+
let plain = true;
|
|
3807
|
+
const visit = (v) => {
|
|
3808
|
+
if (!plain)
|
|
3809
|
+
return;
|
|
3810
|
+
if (Array.isArray(v)) {
|
|
3811
|
+
for (const x of v)
|
|
3812
|
+
visit(x);
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
if (v === null || typeof v !== "object")
|
|
3816
|
+
return;
|
|
3817
|
+
if (isNode(v)) {
|
|
3818
|
+
if (kindOf(v) === "SubLink") {
|
|
3819
|
+
plain = false;
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
if (kindOf(v) === "ColumnRef") {
|
|
3823
|
+
const t = this.typed(v)?.type;
|
|
3824
|
+
// an array (a list to DuckDB, whose step it plans as its join) or a row (a struct) reads as no column a
|
|
3825
|
+
// step could hold; a user type's array is named `_<type>` in pg_type. A scalar user type (an enum, a
|
|
3826
|
+
// domain, citext) is a column like any other
|
|
3827
|
+
if (t === undefined || ARRAYS.has(t) || this.enums.arrays.has(t) || this.isRowType(t) || this.userTypes.get(t)?.startsWith("_") === true || t === INET_ARRAY) {
|
|
3828
|
+
plain = false;
|
|
3829
|
+
return;
|
|
3830
|
+
}
|
|
3831
|
+
seen.add(JSON.stringify(names(bodyOf(v).fields)));
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
for (const x of Object.values(isNode(v) ? bodyOf(v) : v))
|
|
3836
|
+
visit(x);
|
|
3837
|
+
};
|
|
3838
|
+
visit(arg);
|
|
3839
|
+
return plain ? seen.size : null;
|
|
3840
|
+
}
|
|
3841
|
+
/**
|
|
3842
|
+
* The joins whose every column the answer reads (leftLateral): those in the FROM of a SELECT whose list holds a bare
|
|
3843
|
+
* `*`, and those inside a join that has a name of its own (`(a JOIN b) AS j`, read whole by `j.*` or `j`). A column a
|
|
3844
|
+
* step added to one would be a column of the answer that Postgres's has not.
|
|
3845
|
+
*/
|
|
3846
|
+
markWholeReads(root) {
|
|
3847
|
+
const joins = (x) => {
|
|
3848
|
+
if (!isNode(x) || kindOf(x) !== "JoinExpr")
|
|
3849
|
+
return;
|
|
3850
|
+
const j = bodyOf(x);
|
|
3851
|
+
this.readWhole.add(j);
|
|
3852
|
+
joins(j.larg);
|
|
3853
|
+
joins(j.rarg);
|
|
3854
|
+
};
|
|
3855
|
+
const bareStar = (t) => {
|
|
3856
|
+
const v = isNode(t) ? bodyOf(t).val : undefined;
|
|
3857
|
+
if (!isNode(v) || kindOf(v) !== "ColumnRef")
|
|
3858
|
+
return false;
|
|
3859
|
+
const fields = (bodyOf(v).fields ?? []);
|
|
3860
|
+
return fields.length === 1 && kindOf(fields[0]) === "A_Star";
|
|
3861
|
+
};
|
|
3862
|
+
const visit = (v) => {
|
|
3863
|
+
if (Array.isArray(v)) {
|
|
3864
|
+
for (const x of v)
|
|
3865
|
+
visit(x);
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (v === null || typeof v !== "object")
|
|
3869
|
+
return;
|
|
3870
|
+
if (isNode(v)) {
|
|
3871
|
+
const b = bodyOf(v);
|
|
3872
|
+
if (kindOf(v) === "SelectStmt" && (b.targetList ?? []).some(bareStar))
|
|
3873
|
+
for (const f of (b.fromClause ?? []))
|
|
3874
|
+
joins(f);
|
|
3875
|
+
if (kindOf(v) === "JoinExpr" && b.alias !== undefined)
|
|
3876
|
+
joins(v);
|
|
3877
|
+
}
|
|
3878
|
+
for (const [k, x] of Object.entries(isNode(v) ? bodyOf(v) : v))
|
|
3879
|
+
visit(armOf(k, x) ?? x);
|
|
3880
|
+
};
|
|
3881
|
+
visit(root);
|
|
3882
|
+
}
|
|
2924
3883
|
rangeFunction(b) {
|
|
2925
3884
|
const items = (b.functions ?? []);
|
|
2926
3885
|
const first = items[0] !== undefined ? bodyOf(items[0]).items?.[0] : undefined;
|
|
@@ -2928,7 +3887,9 @@ class Rewriter {
|
|
|
2928
3887
|
return null;
|
|
2929
3888
|
const call = bodyOf(first);
|
|
2930
3889
|
const name = lastName(call.funcname);
|
|
2931
|
-
|
|
3890
|
+
// a LEFT lateral's argument computed by a step of its own before it (leftLateral): its column, in its place
|
|
3891
|
+
const stepped = this.stepArg.get(b);
|
|
3892
|
+
const args = stepped !== undefined ? [stepped] : (call.args ?? []);
|
|
2932
3893
|
const alias = b.alias;
|
|
2933
3894
|
const aliasName = alias?.aliasname;
|
|
2934
3895
|
const colnames = names(alias?.colnames);
|
|
@@ -2937,6 +3898,8 @@ class Rewriter {
|
|
|
2937
3898
|
if (isNode(f) && kindOf(f) === "FuncCall")
|
|
2938
3899
|
this.inFrom.add(bodyOf(f));
|
|
2939
3900
|
const canonical = name.startsWith("jsonb");
|
|
3901
|
+
// the right side of a LEFT JOIN … ON TRUE whose rows are a list: written never empty (leftLateral)
|
|
3902
|
+
const orNull = this.nonEmpty.has(b);
|
|
2940
3903
|
switch (name) {
|
|
2941
3904
|
// jsonpath's rows, as Postgres finds them (src/duck/pg-eval.ts), unnested
|
|
2942
3905
|
case "jsonb_path_query": {
|
|
@@ -2956,11 +3919,14 @@ class Rewriter {
|
|
|
2956
3919
|
if (b.ordinality === true)
|
|
2957
3920
|
throw new Unrewritable(`${name}() WITH ORDINALITY is not supported by Mason`, "number the elements with row_number() OVER () instead", "with-ordinality");
|
|
2958
3921
|
const text = name.endsWith("_text");
|
|
2959
|
-
const elements = fn("__mason_json_elements", [jsonArg(args[0]), boolConst(canonical), boolConst(text)]);
|
|
3922
|
+
const elements = fn("__mason_json_elements", [jsonArg(args[0]), boolConst(canonical), boolConst(text), boolConst(orNull)]);
|
|
2960
3923
|
const list = text ? elements : arrayCast(elements, "json");
|
|
2961
|
-
//
|
|
2962
|
-
|
|
3924
|
+
// its column is its OUT parameter's, `value`, under a FROM name too (`e.value`), unless the alias names it;
|
|
3925
|
+
// the FROM name alone is the element (the ColumnRef rule: Typed.functionColumn)
|
|
3926
|
+
const column = colnames[0] ?? "value";
|
|
2963
3927
|
const unnest = fn("unnest", [list]);
|
|
3928
|
+
if (orNull)
|
|
3929
|
+
return listLateral([{ name: column, list }], aliasName ?? name, this.done);
|
|
2964
3930
|
const item = bodyOf(items[0]);
|
|
2965
3931
|
return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [unnest, ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
|
|
2966
3932
|
}
|
|
@@ -3007,32 +3973,141 @@ class Rewriter {
|
|
|
3007
3973
|
throw new Unrewritable(`${name}() WITH ORDINALITY is not supported by Mason`, "number the members with row_number() OVER () instead", "with-ordinality");
|
|
3008
3974
|
const text = name.endsWith("_text");
|
|
3009
3975
|
// each member's key and value, two lists of one length that unnest side by side
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
],
|
|
3015
|
-
op: "SETOP_NONE",
|
|
3016
|
-
};
|
|
3017
|
-
this.done.add(sub);
|
|
3018
|
-
return { RangeSubselect: { lateral: true, subquery: { SelectStmt: sub }, alias: { aliasname: aliasName ?? name } } };
|
|
3976
|
+
return listLateral([
|
|
3977
|
+
{ name: colnames[0] ?? "key", list: fn("__mason_json_keys", [jsonArg(args[0]), boolConst(canonical), boolConst(orNull)]) },
|
|
3978
|
+
{ name: colnames[1] ?? "value", list: fn("__mason_json_values", [this.clone(args[0]), boolConst(canonical), boolConst(text), boolConst(orNull)]) },
|
|
3979
|
+
], aliasName ?? name, this.done);
|
|
3019
3980
|
}
|
|
3020
3981
|
case "jsonb_object_keys":
|
|
3021
3982
|
case "json_object_keys": {
|
|
3022
3983
|
const column = colnames[0] ?? aliasName ?? name;
|
|
3023
|
-
const
|
|
3984
|
+
const keys = fn("__mason_json_keys", [args[0], boolConst(canonical), boolConst(orNull)]);
|
|
3985
|
+
if (orNull)
|
|
3986
|
+
return listLateral([{ name: column, list: keys }], aliasName ?? name, this.done);
|
|
3024
3987
|
const item = bodyOf(items[0]);
|
|
3025
|
-
return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [unnest, ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
|
|
3988
|
+
return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [fn("unnest", [keys]), ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
|
|
3989
|
+
}
|
|
3990
|
+
// an array's elements, never empty: its list where it has one, else one NULL (leftLateral). The CASE names the
|
|
3991
|
+
// list twice: a column's (or the column of the step leftLateral computed the expression in) costs nothing
|
|
3992
|
+
// twice; an expression left here (it reads an array, holds a subquery, or a `*` reads the join) would be
|
|
3993
|
+
// computed twice (a call of Mason's string_to_array a row more), so it is computed ONCE, the one element of a
|
|
3994
|
+
// list whose lambda names it twice: `list_transform(list_value(<expr>), __mason_l -> CASE … __mason_l …
|
|
3995
|
+
// END)[1]`, a copy of its list a row more, the same streaming UNNEST (measured: docs/DESIGN.md "A LEFT lateral
|
|
3996
|
+
// of a list")
|
|
3997
|
+
case "unnest": {
|
|
3998
|
+
if (!orNull)
|
|
3999
|
+
return null;
|
|
4000
|
+
const list = args[0];
|
|
4001
|
+
const column = colnames[0] ?? aliasName ?? name;
|
|
4002
|
+
const never = (l) => caseWhen([[op(">", fn("len", [l()]), intConst(0)), l()]], arrayOf([nullConst()]));
|
|
4003
|
+
const each = () => ({ ColumnRef: { fields: [S("__mason_l")] } });
|
|
4004
|
+
const once = kindOf(list) === "ColumnRef" ? never(() => this.clone(list))
|
|
4005
|
+
: { A_Indirection: { arg: fn("list_transform", [fn("list_value", [list]), op("->", each(), never(each))]), indirection: [{ A_Indices: { uidx: intConst(1) } }] } };
|
|
4006
|
+
return listLateral([{ name: column, list: once }], aliasName ?? name, this.done);
|
|
3026
4007
|
}
|
|
3027
4008
|
default: return null;
|
|
3028
4009
|
}
|
|
3029
4010
|
}
|
|
3030
4011
|
}
|
|
4012
|
+
/**
|
|
4013
|
+
* THE NULLS-PLACEMENT IDIOM AS ONE KEY (docs/DESIGN.md "Nulls placed by a key of their own"): `ORDER BY x IS NULL,
|
|
4014
|
+
* x DESC` — nulls last, written without NULLS LAST, as SQL that must also run where NULLS LAST is not grammar — orders
|
|
4015
|
+
* exactly as `ORDER BY x DESC NULLS LAST`. DuckDB's top N cannot push its threshold into the scan of an expression key,
|
|
4016
|
+
* so it read every row of a 197 M-row copy for a 100-row page (6.4 s by a timestamp; 91 s by a text id, cut off by
|
|
4017
|
+
* the tool); one plain key with its NULLS placement is pruned by the zone maps. So a pair of CONSECUTIVE keys, the
|
|
4018
|
+
* first `<e> IS [NOT] NULL` (either direction, no USING) and the second the same `<e>` (the same tree, parentheses
|
|
4019
|
+
* being no node of it), is one key on `<e>` with the second's direction and the nulls where the first put them: `IS
|
|
4020
|
+
* NULL ASC` and `IS NOT NULL DESC` last, `IS NULL DESC` and `IS NOT NULL ASC` first. Every other key stays in place.
|
|
4021
|
+
* Not under a DISTINCT ON, whose expressions must lead the ORDER BY as written, and never a window's ORDER BY.
|
|
4022
|
+
*/
|
|
4023
|
+
function nullsKeyFolded(keys, select) {
|
|
4024
|
+
const distinctOn = Array.isArray(select.distinctClause) && select.distinctClause.some((d) => isNode(d));
|
|
4025
|
+
if (distinctOn || keys.length < 2)
|
|
4026
|
+
return keys;
|
|
4027
|
+
const out = [];
|
|
4028
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4029
|
+
const first = keys[i], second = keys[i + 1];
|
|
4030
|
+
const a = kindOf(first) === "SortBy" ? bodyOf(first) : null, b = second !== undefined && kindOf(second) === "SortBy" ? bodyOf(second) : null;
|
|
4031
|
+
const test = a !== null && isNode(a.node) && kindOf(a.node) === "NullTest" ? bodyOf(a.node) : null;
|
|
4032
|
+
if (a === null || b === null || test === null || a.useOp !== undefined || b.useOp !== undefined || !isNode(test.arg) || !alikeTree(test.arg, b.node)
|
|
4033
|
+
|| !bareKeyIsInput(test.arg, b.node, select)) {
|
|
4034
|
+
out.push(first);
|
|
4035
|
+
continue;
|
|
4036
|
+
}
|
|
4037
|
+
const last = (test.nulltesttype === "IS_NULL") === (a.sortby_dir !== "SORTBY_DESC");
|
|
4038
|
+
out.push({ SortBy: { ...b, sortby_nulls: last ? "SORTBY_NULLS_LAST" : "SORTBY_NULLS_FIRST" } });
|
|
4039
|
+
i++;
|
|
4040
|
+
}
|
|
4041
|
+
return out;
|
|
4042
|
+
}
|
|
4043
|
+
/**
|
|
4044
|
+
* Whether an ORDER BY key read BARE is the same value as the same tree read inside an expression (the `x` of `x IS
|
|
4045
|
+
* NULL`), where Postgres reads them by two rules (findTargetlistEntrySQL92, parse_clause.c): inside an expression a name
|
|
4046
|
+
* is always an input column, while a bare key that is an integer constant is an output column by its position (`ORDER
|
|
4047
|
+
* BY 2`), and a bare one-part name that is an output column's name is that output column (`SELECT coalesce(x, 0) AS x
|
|
4048
|
+
* … ORDER BY x`). Every other node is read alike either way. So a constant is never, and a one-part name is only
|
|
4049
|
+
* where no output column may bear its name or every one that may is the same tree as the input's (`SELECT x … ORDER BY
|
|
4050
|
+
* x`). An output column's name is its alias, else Postgres's FigureColname (a CASE's may be its ELSE's).
|
|
4051
|
+
*/
|
|
4052
|
+
function bareKeyIsInput(inner, bare, select) {
|
|
4053
|
+
if (kindOf(bare) === "A_Const")
|
|
4054
|
+
return false;
|
|
4055
|
+
if (kindOf(bare) !== "ColumnRef")
|
|
4056
|
+
return true;
|
|
4057
|
+
const fields = (bodyOf(bare).fields ?? []);
|
|
4058
|
+
if (fields.length !== 1 || kindOf(fields[0]) !== "String")
|
|
4059
|
+
return true;
|
|
4060
|
+
const name = String(bodyOf(fields[0]).sval);
|
|
4061
|
+
return (select.targetList ?? []).every((t) => {
|
|
4062
|
+
const r = bodyOf(t), val = r.val;
|
|
4063
|
+
if (val === undefined)
|
|
4064
|
+
return true;
|
|
4065
|
+
// `*` and `t.*` are the input columns themselves
|
|
4066
|
+
const last = kindOf(val) === "ColumnRef" ? (bodyOf(val).fields ?? []).at(-1) : undefined;
|
|
4067
|
+
if (last !== undefined && kindOf(last) === "A_Star")
|
|
4068
|
+
return true;
|
|
4069
|
+
// every target is named by now: its alias, else Postgres's name for it (nameTargets, figureColname)
|
|
4070
|
+
return (typeof r.name === "string" && r.name !== name) || alikeTree(val, inner);
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
/** two trees alike but for where their text stood (`location`): `(x)` and `x` are one tree, parentheses no node */
|
|
4074
|
+
function alikeTree(a, b) {
|
|
4075
|
+
if (Array.isArray(a))
|
|
4076
|
+
return Array.isArray(b) && a.length === b.length && a.every((x, i) => alikeTree(x, b[i]));
|
|
4077
|
+
if (a === null || typeof a !== "object")
|
|
4078
|
+
return a === b;
|
|
4079
|
+
if (b === null || typeof b !== "object" || Array.isArray(b))
|
|
4080
|
+
return false;
|
|
4081
|
+
const ka = Object.keys(a).filter((k) => k !== "location"), kb = Object.keys(b).filter((k) => k !== "location");
|
|
4082
|
+
return ka.length === kb.length && ka.every((k) => alikeTree(a[k], b[k]));
|
|
4083
|
+
}
|
|
4084
|
+
/** the json functions whose rows Mason writes as lists in FROM (src/duck/rewrite.ts rangeFunction), each with an
|
|
4085
|
+
* `orNull` form (src/duck/functions.ts) */
|
|
4086
|
+
const LIST_ROWS = new Set(["jsonb_each", "json_each", "jsonb_each_text", "json_each_text", "jsonb_array_elements", "json_array_elements",
|
|
4087
|
+
"jsonb_array_elements_text", "json_array_elements_text", "jsonb_object_keys", "json_object_keys"]);
|
|
4088
|
+
/** `LATERAL (SELECT unnest(<list>) AS <name>, …) AS <alias>`: lists of one length unnested side by side, a subquery
|
|
4089
|
+
* DuckDB runs as its UNNEST operator when the join is INNER (leftLateral) */
|
|
4090
|
+
function listLateral(columns, alias, done) {
|
|
4091
|
+
const sub = { targetList: columns.map((c) => ({ ResTarget: { name: c.name, val: fn("unnest", [c.list]) } })), op: "SETOP_NONE" };
|
|
4092
|
+
done.add(sub);
|
|
4093
|
+
return { RangeSubselect: { lateral: true, subquery: { SelectStmt: sub }, alias: { aliasname: alias } } };
|
|
4094
|
+
}
|
|
3031
4095
|
/** a cell an exact program cannot take (in a grouped SELECT, a column not grouped): the column stays DuckDB's */
|
|
3032
4096
|
const NOT_EXACT = new Error("not exact");
|
|
3033
4097
|
/** a SELECT list item that is `*` or `t.*` */
|
|
3034
4098
|
/** a set operation's arm (libpg-query's `larg` / `rarg`: a SelectStmt's body, unwrapped), as the node it is */
|
|
3035
4099
|
const armOf = (key, v) => (key === "larg" || key === "rarg") && v !== null && typeof v === "object" && !Array.isArray(v) && !isNode(v) ? { SelectStmt: v } : null;
|
|
4100
|
+
/** the aggregates whose answer follows the order their rows come in, without an ORDER BY of their own (readsInOrder) */
|
|
4101
|
+
const IN_ORDER_AGGREGATES = new Set(["string_agg", "array_agg", "json_agg", "jsonb_agg", "json_agg_strict", "jsonb_agg_strict", "json_object_agg",
|
|
4102
|
+
"jsonb_object_agg", "json_object_agg_strict", "jsonb_object_agg_strict", "json_object_agg_unique", "jsonb_object_agg_unique",
|
|
4103
|
+
"json_object_agg_unique_strict", "jsonb_object_agg_unique_strict", "xmlagg"]);
|
|
4104
|
+
/** the window functions that read their frame in order: over a window with no ORDER BY, the order the rows come in */
|
|
4105
|
+
const IN_ORDER_WINDOWS = new Set(["row_number", "ntile", "lag", "lead", "first_value", "last_value", "nth_value"]);
|
|
4106
|
+
/** whether an expression holds a window function (outside a subquery) */
|
|
4107
|
+
const hasWindow = (v) => Array.isArray(v) ? v.some(hasWindow)
|
|
4108
|
+
: v === null || typeof v !== "object" ? false
|
|
4109
|
+
: isNode(v) ? kindOf(v) !== "SubLink" && ((kindOf(v) === "FuncCall" && bodyOf(v).over !== undefined) || Object.values(bodyOf(v)).some(hasWindow))
|
|
4110
|
+
: Object.values(v).some(hasWindow);
|
|
3036
4111
|
const hasStar = (v) => isNode(v) && kindOf(v) === "ColumnRef" && bodyOf(v).fields.some((f) => kindOf(f) === "A_Star");
|
|
3037
4112
|
/** whether a program does what DuckDB would compute otherwise: a division, a rounding, a scale chosen per value; a
|
|
3038
4113
|
* sum, a difference or a product of DuckDB's decimals is DuckDB's own right answer */
|
|
@@ -3640,6 +4715,24 @@ function replacement(r) {
|
|
|
3640
4715
|
return strConst(s.replace(/\\(.)/g, (_m, c) => c === "&" ? "\\0" : `\\${c}`));
|
|
3641
4716
|
}
|
|
3642
4717
|
/** substring(text FROM pattern): the first parenthesised part when there is one, else the whole match; NULL without one */
|
|
4718
|
+
/**
|
|
4719
|
+
* `current_setting(name[, missing_ok])` BESIDE A COPIED TABLE IS THE SESSION'S (GitHub issue 40): the name read, in any
|
|
4720
|
+
* case, from the settings the session's statement runs under (`SETTINGS_VARIABLE`: src/api/postgres/server.ts
|
|
4721
|
+
* `duckSession`, the listener's `sessionSetting` laid over the catalog Postgres's own, set by src/duck/store.ts
|
|
4722
|
+
* `settle`) — never DuckDB's own `current_setting`, which answered DuckDB's settings (`search_path` its schemas,
|
|
4723
|
+
* `work_mem` an error). A name neither holds is Postgres's 42704, or NULL under missing_ok; strict, as Postgres's.
|
|
4724
|
+
*/
|
|
4725
|
+
function settingRead(copy, args) {
|
|
4726
|
+
const [name, missingOk] = args;
|
|
4727
|
+
const settings = () => fn("getvariable", [strConst(SETTINGS_VARIABLE)]);
|
|
4728
|
+
const key = () => fn("lower", [copy(name)]);
|
|
4729
|
+
const unknown = fn("error", [concat(strConst('mason[42704]: unrecognized configuration parameter "'), copy(name), strConst('"'))]);
|
|
4730
|
+
return caseWhen([
|
|
4731
|
+
[missingOk === undefined ? isNull(name) : or([isNull(name), isNull(missingOk)]), nullConst()],
|
|
4732
|
+
[fn("map_contains", [settings(), key()]), fn("map_extract_value", [settings(), key()])],
|
|
4733
|
+
...(missingOk === undefined ? [] : [[copy(missingOk), nullConst()]]),
|
|
4734
|
+
], unknown);
|
|
4735
|
+
}
|
|
3643
4736
|
function regexSubstring(copy, s, p) {
|
|
3644
4737
|
const pattern = regexPattern(p, true);
|
|
3645
4738
|
const text = constString(p);
|
|
@@ -3749,29 +4842,51 @@ function strptimeFormat(fmt) {
|
|
|
3749
4842
|
* …) s`). Every unnamed target is given this name (`nameTargets`).
|
|
3750
4843
|
*/
|
|
3751
4844
|
export function figureColname(n) {
|
|
4845
|
+
const [name, strength] = colnameOf(n);
|
|
4846
|
+
return strength === 0 ? "?column?" : name;
|
|
4847
|
+
}
|
|
4848
|
+
/** FigureColnameInternal: the name and its strength — 2 a name of the expression's own (a column's, a function's), 1 a
|
|
4849
|
+
* kind's (`case`, a cast's type), 0 none. A CASE is named by its ELSE where that name is strong (`CASE … ELSE n END`
|
|
4850
|
+
* is `n`), and a cast by its argument where that one's is, else by its type */
|
|
4851
|
+
function colnameOf(n) {
|
|
3752
4852
|
const k = kindOf(n), b = bodyOf(n);
|
|
3753
4853
|
switch (k) {
|
|
3754
|
-
case "
|
|
3755
|
-
const
|
|
3756
|
-
|
|
3757
|
-
|
|
4854
|
+
case "TypeCast": {
|
|
4855
|
+
const inner = isNode(b.arg) ? colnameOf(b.arg) : ["?column?", 0];
|
|
4856
|
+
return inner[1] > 1 ? inner : [lastName(b.typeName?.names), 1];
|
|
4857
|
+
}
|
|
4858
|
+
case "CollateClause": return isNode(b.arg) ? colnameOf(b.arg) : ["?column?", 0];
|
|
4859
|
+
case "CaseExpr": {
|
|
4860
|
+
const d = isNode(b.defresult) ? colnameOf(b.defresult) : ["?column?", 0];
|
|
4861
|
+
return d[1] > 1 ? d : ["case", 1];
|
|
3758
4862
|
}
|
|
4863
|
+
case "A_Expr": return b.kind === "AEXPR_NULLIF" ? ["nullif", 2] : ["?column?", 0];
|
|
3759
4864
|
case "A_Indirection": {
|
|
3760
4865
|
const ind = (b.indirection ?? []);
|
|
3761
4866
|
for (let i = ind.length - 1; i >= 0; i--) {
|
|
3762
4867
|
if (kindOf(ind[i]) === "String")
|
|
3763
|
-
return String(bodyOf(ind[i]).sval);
|
|
4868
|
+
return [String(bodyOf(ind[i]).sval), 2];
|
|
3764
4869
|
if (kindOf(ind[i]) === "A_Star")
|
|
3765
4870
|
break;
|
|
3766
4871
|
}
|
|
3767
|
-
return isNode(b.arg) ?
|
|
4872
|
+
return isNode(b.arg) ? colnameOf(b.arg) : ["?column?", 0];
|
|
3768
4873
|
}
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
4874
|
+
default: {
|
|
4875
|
+
const name = strongColname(n);
|
|
4876
|
+
return name === "?column?" ? [name, 0] : [name, 2];
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
/** the kinds whose name is their own (strength 2), or none */
|
|
4881
|
+
function strongColname(n) {
|
|
4882
|
+
const k = kindOf(n), b = bodyOf(n);
|
|
4883
|
+
switch (k) {
|
|
4884
|
+
case "ColumnRef": {
|
|
4885
|
+
const f = (b.fields ?? []);
|
|
4886
|
+
const last = f[f.length - 1];
|
|
4887
|
+
return last !== undefined && kindOf(last) === "String" ? String(bodyOf(last).sval) : "?column?";
|
|
3773
4888
|
}
|
|
3774
|
-
case "
|
|
4889
|
+
case "FuncCall": return lastName(b.funcname);
|
|
3775
4890
|
case "GroupingFunc": return "grouping";
|
|
3776
4891
|
case "SubLink": {
|
|
3777
4892
|
if (b.subLinkType === "EXISTS_SUBLINK")
|
|
@@ -3783,7 +4898,6 @@ export function figureColname(n) {
|
|
|
3783
4898
|
const first = (bodyOf(b.subselect).targetList ?? [])[0];
|
|
3784
4899
|
return first !== undefined ? (bodyOf(first).name ?? (isNode(bodyOf(first).val) ? figureColname(bodyOf(first).val) : "?column?")) : "?column?";
|
|
3785
4900
|
}
|
|
3786
|
-
case "CaseExpr": return "case";
|
|
3787
4901
|
case "A_ArrayExpr": return "array";
|
|
3788
4902
|
case "RowExpr": return "row";
|
|
3789
4903
|
case "CoalesceExpr": return "coalesce";
|
|
@@ -3823,16 +4937,22 @@ function nameTargets(v) {
|
|
|
3823
4937
|
for (const [k, x] of Object.entries(b))
|
|
3824
4938
|
nameTargets(armOf(k, x) ?? x);
|
|
3825
4939
|
}
|
|
3826
|
-
export function rewrite(stmt, base, analysis, collation, userTypes = new Map(), rowTypes = new Map(), timeZone = "UTC", enums = { labels: new Map(), arrays: new Map() }, kept = null) {
|
|
3827
|
-
const r = new Rewriter(analysis, base, collation, userTypes, rowTypes, timeZone, enums, kept);
|
|
4940
|
+
export function rewrite(stmt, base, analysis, collation, userTypes = new Map(), rowTypes = new Map(), timeZone = "UTC", enums = { labels: new Map(), arrays: new Map() }, kept = null, wireBound = false, keyCounts = new Set()) {
|
|
4941
|
+
const r = new Rewriter(analysis, base, collation, userTypes, rowTypes, timeZone, enums, kept, keyCounts);
|
|
4942
|
+
if (joinsUsing(analysis.tree, new Set([JSONB, JSONB_ARRAY]))) {
|
|
4943
|
+
throw new Unrewritable("a JOIN … USING or a NATURAL JOIN on a jsonb column is not supported by Mason (Postgres equates jsonb by its own rules)", "JOIN … ON a.j = b.j", "jsonb-compare");
|
|
4944
|
+
}
|
|
3828
4945
|
const own = JSON.parse(JSON.stringify(stmt));
|
|
3829
4946
|
volatileWindows(own);
|
|
3830
4947
|
r.adopt(own);
|
|
3831
4948
|
nameTargets(own);
|
|
3832
4949
|
r.indexShapes(own);
|
|
4950
|
+
r.repeatedKeyCounts(own);
|
|
3833
4951
|
const exact = r.exactThrough(own, analysis.fields);
|
|
4952
|
+
r.markBounds(own, wireBound);
|
|
4953
|
+
r.markWholeReads(own);
|
|
3834
4954
|
const tree = guardFilters(flattenBool(r.walk(own)));
|
|
3835
|
-
return { tree, dropLast: r.excludeRank, exact, clock: r.clock, jsonReads: [...r.jsonReads.values()] };
|
|
4955
|
+
return { tree, dropLast: r.excludeRank, exact, clock: r.clock, jsonReads: [...r.jsonReads.values()], unboundedLateral: r.unboundedLateral };
|
|
3836
4956
|
}
|
|
3837
4957
|
/** the functions whose every call is a new value (Postgres's volatile ones Mason answers) */
|
|
3838
4958
|
const VOLATILE = new Set(["random", "gen_random_uuid", "uuid_generate_v4", "clock_timestamp", "timeofday"]);
|