@mason-data/cli 0.0.0-stage → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +209 -2
- package/dist/src/api/error.js +18 -0
- package/dist/src/api/postgres/catalog-worker.js +92 -0
- package/dist/src/api/postgres/catalog.js +442 -0
- package/dist/src/api/postgres/encode.js +541 -0
- package/dist/src/api/postgres/explain.js +143 -0
- package/dist/src/api/postgres/listener.js +60 -0
- package/dist/src/api/postgres/parser-worker.js +58 -0
- package/dist/src/api/postgres/parser.js +121 -0
- package/dist/src/api/postgres/plain.js +33 -0
- package/dist/src/api/postgres/protocol.js +404 -0
- package/dist/src/api/postgres/schema.js +54 -0
- package/dist/src/api/postgres/server.js +1670 -0
- package/dist/src/api/postgres/translate.js +248 -0
- package/dist/src/api/request-log.js +50 -0
- package/dist/src/cdc/pg-wire.js +482 -0
- package/dist/src/cdc/pgoutput.js +107 -0
- package/dist/src/cdc/poll.js +67 -0
- package/dist/src/cdc/source.js +65 -0
- package/dist/src/cli/background.js +174 -0
- package/dist/src/cli/command.js +172 -0
- package/dist/src/cli/commands.js +1145 -0
- package/dist/src/cli/control.js +36 -0
- package/dist/src/cli/dev.js +576 -0
- package/dist/src/cli/doctor.js +257 -0
- package/dist/src/cli/guides.js +134 -0
- package/dist/src/cli/init.js +326 -0
- package/dist/src/cli/machine.js +157 -0
- package/dist/src/cli/main.js +83 -0
- package/dist/src/cli/pg-tls.js +100 -0
- package/dist/src/cli/pg-url.js +52 -0
- package/dist/src/cli/pin.js +36 -0
- package/dist/src/cli/ports.js +118 -0
- package/dist/src/cli/preflight.js +338 -0
- package/dist/src/cli/project.js +127 -0
- package/dist/src/cli/readiness.js +76 -0
- package/dist/src/cli/registry.js +217 -0
- package/dist/src/cli/replication-checks.js +120 -0
- package/dist/src/cli/replication-mode.js +148 -0
- package/dist/src/cli/run-server.js +388 -0
- package/dist/src/cli/runs.js +120 -0
- package/dist/src/cli/security.js +86 -0
- package/dist/src/cli/self-signed-cert.js +64 -0
- package/dist/src/cli/serve.js +296 -0
- package/dist/src/cli/sources.js +562 -0
- package/dist/src/cli/state.js +58 -0
- package/dist/src/cli/style.js +187 -0
- package/dist/src/cli/supervise.js +248 -0
- package/dist/src/cli/sync.js +94 -0
- package/dist/src/cli/ui.js +107 -0
- package/dist/src/cli/user.js +229 -0
- package/dist/src/cli/version.js +23 -0
- package/dist/src/duck/analyze.js +113 -0
- package/dist/src/duck/cancel.js +49 -0
- package/dist/src/duck/executor-worker.js +263 -0
- package/dist/src/duck/executor.js +526 -0
- package/dist/src/duck/explain.js +119 -0
- package/dist/src/duck/functions.js +764 -0
- package/dist/src/duck/gaps.js +89 -0
- package/dist/src/duck/jsonb.js +186 -0
- package/dist/src/duck/kept-route.js +219 -0
- package/dist/src/duck/kept.js +797 -0
- package/dist/src/duck/late.js +221 -0
- package/dist/src/duck/libc-order.js +190 -0
- package/dist/src/duck/pg-eval-worker.js +34 -0
- package/dist/src/duck/pg-eval.js +59 -0
- package/dist/src/duck/pgtree.js +338 -0
- package/dist/src/duck/plan.js +246 -0
- package/dist/src/duck/publication.js +207 -0
- package/dist/src/duck/requests.js +209 -0
- package/dist/src/duck/resolved.js +248 -0
- package/dist/src/duck/rewrite.js +4001 -0
- package/dist/src/duck/store.js +831 -0
- package/dist/src/duck/stream.js +518 -0
- package/dist/src/duck/sync.js +2864 -0
- package/dist/src/duck/to-char.js +482 -0
- package/dist/src/duck/token.js +31 -0
- package/dist/src/duck/types.js +427 -0
- package/dist/src/duck/users.js +231 -0
- package/dist/src/duck/verified.js +995 -0
- package/dist/src/duck/views.js +150 -0
- package/dist/src/lib/database-url.js +74 -0
- package/dist/src/lib/did-you-mean.js +43 -0
- package/dist/src/lib/dotenv.js +72 -0
- package/dist/src/lib/exit-when-written.js +20 -0
- package/dist/src/lib/float-equal.js +69 -0
- package/dist/src/lib/http-json.js +22 -0
- package/dist/src/lib/log.js +161 -0
- package/dist/src/lib/pg-numeric.js +489 -0
- package/dist/src/lib/port.js +22 -0
- package/dist/src/lib/project-file.js +49 -0
- package/dist/src/lib/project-files.js +43 -0
- package/dist/src/lib/project-version.js +76 -0
- package/dist/src/lib/runnable.js +14 -0
- package/dist/src/lib/shell-word.js +4 -0
- package/dist/src/lib/timezone.js +236 -0
- package/dist/src/lib/zone-names.js +114 -0
- package/dist/src/web/app.css +65 -0
- package/dist/src/web/app.js +336 -0
- package/dist/src/web/index.html +39 -0
- package/package.json +22 -4
|
@@ -0,0 +1,4001 @@
|
|
|
1
|
+
import { pathIndex, SESSION_VARIABLE } from "./functions.js";
|
|
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 { atTimeZoneOf } from "../lib/timezone.js";
|
|
6
|
+
/** Whether pg_database's collation is a libc locale's (not C or POSIX), whose order is read from the database
|
|
7
|
+
* (src/duck/libc-order.ts) for `duckCollation`'s `libc` */
|
|
8
|
+
export const isLibcCollation = (provider, collate) => provider !== "b" && provider !== "i" && !/^(C|POSIX)([.].*)?$/i.test(collate);
|
|
9
|
+
/** The source's collation as DuckDB follows it, from pg_database: `provider` c (libc), i (ICU) or b (builtin). */
|
|
10
|
+
export function duckCollation(provider, collate, locale, known) {
|
|
11
|
+
const icuName = (l) => {
|
|
12
|
+
const lang = l.replace(/[.@].*$/, "").replace(/-/g, "_").toLowerCase();
|
|
13
|
+
if (known.has(lang))
|
|
14
|
+
return lang;
|
|
15
|
+
const base = lang.split("_")[0];
|
|
16
|
+
return known.has(base) ? base : null;
|
|
17
|
+
};
|
|
18
|
+
if (provider === "b")
|
|
19
|
+
return { kind: "bytes" };
|
|
20
|
+
if (/^(C|POSIX)([.].*)?$/i.test(collate) && provider !== "i")
|
|
21
|
+
return { kind: "bytes", ascii: true };
|
|
22
|
+
if (provider === "i") {
|
|
23
|
+
const name = icuName(locale ?? collate);
|
|
24
|
+
if (name === null)
|
|
25
|
+
throw new Error(`the source database's ICU locale ${JSON.stringify(locale ?? collate)} has no DuckDB collation: Mason cannot sort text as it does`);
|
|
26
|
+
return { kind: "icu", name };
|
|
27
|
+
}
|
|
28
|
+
const name = icuName(collate);
|
|
29
|
+
if (name === null)
|
|
30
|
+
throw new Error(`the source database's collation ${JSON.stringify(collate)} has no DuckDB collation to follow: Mason cannot sort text as it does`);
|
|
31
|
+
return { kind: "libc", icu: name };
|
|
32
|
+
}
|
|
33
|
+
export class Unrewritable extends Error {
|
|
34
|
+
hint;
|
|
35
|
+
gap;
|
|
36
|
+
constructor(message, hint, gap) { super(message); this.hint = hint; this.gap = gap; }
|
|
37
|
+
}
|
|
38
|
+
// --------------------------------------------------------------------------------------------------------- the types
|
|
39
|
+
const INT2 = 21, INT4 = 23, INT8 = 20, FLOAT4 = 700, FLOAT8 = 701, NUMERIC = 1700, TEXT = 25, VARCHAR = 1043, BPCHAR = 1042, NAME = 19;
|
|
40
|
+
const DATE = 1082, TIMESTAMP = 1114, TIMESTAMPTZ = 1184, INTERVAL = 1186, JSON_OID = 114, JSONB = 3802, UNKNOWN = 705, INET = 869, RECORD = 2249;
|
|
41
|
+
const INTEGERS = new Set([INT2, INT4, INT8]);
|
|
42
|
+
const FLOATS = new Set([FLOAT4, FLOAT8]);
|
|
43
|
+
/** the fields of a time that grow with it, ±Infinity of an infinite one (Postgres 14+); the rest are NULL of it */
|
|
44
|
+
const MONOTONIC = new Set(["year", "years", "decade", "decades", "century", "centuries", "millennium", "millennia", "isoyear", "epoch", "julian"]);
|
|
45
|
+
/** an interval field's numeric scale: a second's is 6, a millisecond's 3, an epoch's 6 */
|
|
46
|
+
const INTERVAL_SCALE = { second: "6", seconds: "6", millisecond: "3", milliseconds: "3", epoch: "6" };
|
|
47
|
+
const NUMBER_TYPES = new Set([INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC]);
|
|
48
|
+
/** an array type's element (the numbers', where an IN list's element type decides a comparison) */
|
|
49
|
+
const NUMBER_ELEMENT = { 1005: INT2, 1007: INT4, 1016: INT8, 1021: FLOAT4, 1022: FLOAT8, 1231: NUMERIC };
|
|
50
|
+
const TEXTS = new Set([TEXT, VARCHAR, BPCHAR, NAME, UNKNOWN]);
|
|
51
|
+
const JSONS = new Set([JSON_OID, JSONB]);
|
|
52
|
+
const BOOL = 16, UUID = 2950, TIME = 1083, BYTEA = 17;
|
|
53
|
+
/** pg_type's array types of the element types above */
|
|
54
|
+
const ARRAYS = new Set([1000, 1001, 1005, 1007, 1009, 1014, 1015, 1016, 1021, 1022, 1028, 1115, 1182, 1183, 1185, 1187, 1231, 2951, 199, 3807]);
|
|
55
|
+
/** pg_collation's C and POSIX: byte order */
|
|
56
|
+
const BYTE_COLLATIONS = new Set([950, 951]);
|
|
57
|
+
/** The DuckDB type a Postgres type is cast to (a typmod's numeric(p, s) included), or null where DuckDB has none. */
|
|
58
|
+
function duckTypeOf(oid, typmod = -1) {
|
|
59
|
+
switch (oid) {
|
|
60
|
+
case INT2: return ["smallint"];
|
|
61
|
+
case INT4: return ["integer"];
|
|
62
|
+
case INT8: return ["bigint"];
|
|
63
|
+
case FLOAT4: return ["real"];
|
|
64
|
+
case FLOAT8: return ["double"];
|
|
65
|
+
case NUMERIC: {
|
|
66
|
+
if (typmod < 4)
|
|
67
|
+
return null;
|
|
68
|
+
const p = ((typmod - 4) >> 16) & 0xffff, s = (typmod - 4) & 0xffff;
|
|
69
|
+
return p <= 38 ? ["decimal", String(p), String(s)] : ["double"];
|
|
70
|
+
}
|
|
71
|
+
case TEXT:
|
|
72
|
+
case VARCHAR:
|
|
73
|
+
case BPCHAR:
|
|
74
|
+
case NAME:
|
|
75
|
+
case UNKNOWN: return ["varchar"];
|
|
76
|
+
case 16: return ["boolean"];
|
|
77
|
+
case DATE: return ["date"];
|
|
78
|
+
case TIMESTAMP: return ["timestamp"];
|
|
79
|
+
case TIMESTAMPTZ: return ["timestamptz"];
|
|
80
|
+
case INTERVAL: return ["interval"];
|
|
81
|
+
case 1083: return ["time"];
|
|
82
|
+
case 2950: return ["uuid"];
|
|
83
|
+
case JSON_OID:
|
|
84
|
+
case JSONB: return ["json"];
|
|
85
|
+
case 17: return ["blob"];
|
|
86
|
+
default: return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------------------------------- tree builders
|
|
90
|
+
const S = (s) => ({ String: { sval: s } });
|
|
91
|
+
const strConst = (s) => ({ A_Const: { sval: { sval: s } } });
|
|
92
|
+
const intConst = (n) => ({ A_Const: { ival: n === 0 ? {} : { ival: n } } });
|
|
93
|
+
const numConst = (text) => ({ A_Const: { fval: { fval: text } } });
|
|
94
|
+
const nullConst = () => ({ A_Const: { isnull: true } });
|
|
95
|
+
const boolConst = (v) => ({ A_Const: { boolval: v ? { boolval: true } : {} } });
|
|
96
|
+
const arrayOf = (elements) => ({ A_ArrayExpr: { elements } });
|
|
97
|
+
const or = (args) => ({ BoolExpr: { boolop: "OR_EXPR", args } });
|
|
98
|
+
const isNotNull = (n) => ({ NullTest: { arg: n, nulltesttype: "IS_NOT_NULL" } });
|
|
99
|
+
// a Mason function's first argument is the session (src/duck/functions.ts "every call carries its session"): DuckDB
|
|
100
|
+
// folds `getvariable` to the session's literal at bind time
|
|
101
|
+
const fn = (name, args, extra = {}) => ({
|
|
102
|
+
FuncCall: { funcname: [S(name)], args: name.startsWith("__mason_") ? [fn("getvariable", [strConst(SESSION_VARIABLE)]), ...args] : args, funcformat: "COERCE_EXPLICIT_CALL", ...extra },
|
|
103
|
+
});
|
|
104
|
+
const cast = (arg, type) => ({
|
|
105
|
+
TypeCast: { arg, typeName: { names: [S(type[0])], ...(type.length > 1 ? { typmods: type.slice(1).map((t) => intConst(Number(t))) } : {}), typemod: -1 } },
|
|
106
|
+
});
|
|
107
|
+
/** a value as DuckDB's ENUM of these labels (`CAST(x AS enum('sad', 'ok'))`), or a list of it */
|
|
108
|
+
const enumCast = (arg, labels, list = false) => ({ TypeCast: { arg, typeName: { names: [S("enum")], typmods: labels.map(strConst), typemod: -1,
|
|
109
|
+
...(list ? { arrayBounds: [{ Integer: { ival: -1 } }] } : {}) } } });
|
|
110
|
+
const arrayCast = (arg, element) => ({ TypeCast: { arg, typeName: { names: [S(element)], typemod: -1, arrayBounds: [{ Integer: { ival: -1 } }] } } });
|
|
111
|
+
const op = (name, l, r) => ({ A_Expr: { kind: "AEXPR_OP", name: [S(name)], ...(l === null ? {} : { lexpr: l }), rexpr: r } });
|
|
112
|
+
const not = (n) => ({ BoolExpr: { boolop: "NOT_EXPR", args: [n] } });
|
|
113
|
+
const and = (args) => ({ BoolExpr: { boolop: "AND_EXPR", args } });
|
|
114
|
+
const caseWhen = (arms, otherwise) => ({
|
|
115
|
+
CaseExpr: { args: arms.map(([expr, result]) => ({ CaseWhen: { expr, result } })), ...(otherwise === null ? {} : { defresult: otherwise }) },
|
|
116
|
+
});
|
|
117
|
+
const isNull = (n) => ({ NullTest: { arg: n, nulltesttype: "IS_NULL" } });
|
|
118
|
+
const collate = (n, name) => ({ CollateClause: { arg: n, collname: [S(name)] } });
|
|
119
|
+
const row = (args) => ({ RowExpr: { args, row_format: "COERCE_EXPLICIT_CALL" } });
|
|
120
|
+
const concat = (...parts) => parts.reduce((a, b) => op("||", a, b));
|
|
121
|
+
const kindOf = (n) => Object.keys(n)[0];
|
|
122
|
+
const bodyOf = (n) => n[kindOf(n)];
|
|
123
|
+
const isNode = (v) => v !== null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 1 && /^[A-Z]/.test(Object.keys(v)[0]);
|
|
124
|
+
const names = (list) => Array.isArray(list) ? list.map((x) => isNode(x) && kindOf(x) === "String" ? String(bodyOf(x).sval ?? "") : "") : [];
|
|
125
|
+
/** a function's or type's name without its `pg_catalog.` */
|
|
126
|
+
const lastName = (list) => { const n = names(list); return (n[n.length - 1] ?? "").toLowerCase(); };
|
|
127
|
+
const constString = (n) => n !== undefined && kindOf(n) === "A_Const" && bodyOf(n).sval !== undefined ? String(bodyOf(n).sval.sval ?? "") : null;
|
|
128
|
+
const constInt = (n) => {
|
|
129
|
+
if (n === undefined || kindOf(n) !== "A_Const")
|
|
130
|
+
return null;
|
|
131
|
+
const b = bodyOf(n);
|
|
132
|
+
return b.ival !== undefined ? Number(b.ival.ival ?? 0) : null;
|
|
133
|
+
};
|
|
134
|
+
// ---------------------------------------------------------------------------------------------------- the rewrite
|
|
135
|
+
/** what a raw node kind is analysed into (pgtree.ts's node names), to find ITS entry among those at one offset */
|
|
136
|
+
const ANALYSED = {
|
|
137
|
+
ColumnRef: ["VAR"],
|
|
138
|
+
A_Const: ["CONST"],
|
|
139
|
+
ParamRef: ["PARAM"],
|
|
140
|
+
FuncCall: ["FUNCEXPR", "AGGREF", "WINDOWFUNC", "COERCEVIAIO", "RELABELTYPE", "ARRAYCOERCEEXPR", "OPEXPR", "MINMAXEXPR", "SQLVALUEFUNCTION"],
|
|
141
|
+
A_Expr: ["OPEXPR", "DISTINCTEXPR", "NULLIFEXPR", "SCALARARRAYOPEXPR", "BOOLEXPR", "FUNCEXPR"],
|
|
142
|
+
TypeCast: ["FUNCEXPR", "COERCEVIAIO", "RELABELTYPE", "CONST", "ARRAYCOERCEEXPR", "COERCETODOMAIN"],
|
|
143
|
+
CaseExpr: ["CASEEXPR"], CoalesceExpr: ["COALESCEEXPR"], MinMaxExpr: ["MINMAXEXPR"], SubLink: ["SUBLINK"], NullTest: ["NULLTEST"],
|
|
144
|
+
BoolExpr: ["BOOLEXPR"], A_ArrayExpr: ["ARRAYEXPR"], RowExpr: ["ROWEXPR"], SQLValueFunction: ["SQLVALUEFUNCTION"],
|
|
145
|
+
A_Indirection: ["SUBSCRIPTINGREF", "FIELDSELECT", "FUNCEXPR"], CollateClause: ["COLLATEEXPR"], BooleanTest: ["BOOLEANTEST"],
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* A NODE THE REWRITE PLACED TWICE (the walker's detector, Rewriter.walk): the walker runs a node's rule at its first
|
|
149
|
+
* place only, so a node a rule put in two places is rewritten in one — the json read's column was qualified in the
|
|
150
|
+
* first arm of its CASE alone (42804 from DuckDB; a review finding). Each is a bug of the rule that placed it:
|
|
151
|
+
* it should clone the node per use. Recorded here, never refused (the second place may well need no rule), and held to
|
|
152
|
+
* none by the tests that answer statements (tests/duck-wire.test.ts, tests/duck-corpus.test.ts, the oracle).
|
|
153
|
+
*/
|
|
154
|
+
const PLACED_TWICE = [];
|
|
155
|
+
function placedTwice(kind, b) {
|
|
156
|
+
if (PLACED_TWICE.length < 100)
|
|
157
|
+
PLACED_TWICE.push(`a ${kind} at offset ${typeof b.location === "number" ? b.location : "?"}: ${JSON.stringify(b).slice(0, 160)}`);
|
|
158
|
+
}
|
|
159
|
+
/** the nodes the rewrite placed twice since the process began (at most 100): none, when every rule clones per use */
|
|
160
|
+
export const nodesPlacedTwice = () => PLACED_TWICE;
|
|
161
|
+
class Rewriter {
|
|
162
|
+
types;
|
|
163
|
+
base;
|
|
164
|
+
collation;
|
|
165
|
+
params;
|
|
166
|
+
/** bodies whose rule has run: a rule may wrap its own node, which must not be rewritten twice */
|
|
167
|
+
done = new WeakSet();
|
|
168
|
+
/** the statement's own bodies (and copies of them): only these are rewritten, never a node a rule built */
|
|
169
|
+
original = new WeakSet();
|
|
170
|
+
/** the statement's own bodies the walk has met, each in its one place (walk) */
|
|
171
|
+
placed = new WeakSet();
|
|
172
|
+
/** the bodies whose own rule's answer is being walked: met there, a body is in the place its rule put it */
|
|
173
|
+
answering = new Set();
|
|
174
|
+
/** the function calls a FROM reads (a set-returning function there is a table, not unnest's) */
|
|
175
|
+
inFrom = new WeakSet();
|
|
176
|
+
/** how the database changes a letter's case */
|
|
177
|
+
caseMode;
|
|
178
|
+
/** the ANY / ALL comparisons already guarded against a NULL array (the guard holds the comparison, walked again) */
|
|
179
|
+
nullArrayTested = new WeakSet();
|
|
180
|
+
/** the catalog's enums: each one's labels in their declared order, and its array type's element */
|
|
181
|
+
enums;
|
|
182
|
+
/** the catalog's user types by OID (`citext`), for the few the rewrite knows (src/duck/plan.ts reads pg_type) */
|
|
183
|
+
userTypes;
|
|
184
|
+
/** a copied table's row type: its columns, by the catalog's OID (src/duck/plan.ts) */
|
|
185
|
+
rowTypes;
|
|
186
|
+
/** the session's TimeZone the statement was analysed under (a relative literal's day is its) */
|
|
187
|
+
timeZone;
|
|
188
|
+
/** the stored JSON paths (src/duck/kept-route.ts): null where none is consulted (the feature off, a test) */
|
|
189
|
+
kept;
|
|
190
|
+
/** every read of a copied table's JSON column by literal steps the statement makes, stored or not: the request log's
|
|
191
|
+
* `json_reads`, which the keeper's policy reads (src/duck/kept.ts `review`) */
|
|
192
|
+
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) {
|
|
194
|
+
this.types = analysis.types;
|
|
195
|
+
this.base = base;
|
|
196
|
+
this.collation = collation;
|
|
197
|
+
this.params = analysis.params;
|
|
198
|
+
this.userTypes = userTypes;
|
|
199
|
+
this.rowTypes = rowTypes;
|
|
200
|
+
this.timeZone = timeZone;
|
|
201
|
+
this.enums = enums;
|
|
202
|
+
this.kept = kept;
|
|
203
|
+
this.caseMode = collation.kind === "icu" ? "icu" : collation.kind === "bytes" && collation.ascii === true ? "ascii" : "libc";
|
|
204
|
+
this.twins = caseTwins(analysis.tree);
|
|
205
|
+
}
|
|
206
|
+
/** the subqueries and CTEs whose columns have twins in another case, by the name the query calls them (caseTwins) */
|
|
207
|
+
twins;
|
|
208
|
+
/**
|
|
209
|
+
* A STORED JSON PATH READ FROM ITS COLUMN (docs/DESIGN.md "Stored JSON paths"): `l` a copied table's
|
|
210
|
+
* json or jsonb column (Typed.relation) read by literal `steps` — recorded as one of the statement's reads (as its
|
|
211
|
+
* one path: canonicalRead), and, when every row of its path is stored (READY, src/duck/kept.ts), answered from the
|
|
212
|
+
* hidden column ALONE: the read names no document, so DuckDB reads only the stored column (on the test server a
|
|
213
|
+
* whole-table read through a CASE whose fallback named the document still read every document: 41.6 s, where a small
|
|
214
|
+
* column's scan took 8.5 s). The column holds the path's `->` answer (the value's JSON text) or `KEPT_NULL` (a NULL
|
|
215
|
+
* answer), so `->` is the column with the mark read as NULL, and `->>` is derived from it (`keptText`). Only a path
|
|
216
|
+
* whose column is not EXACT — a row unset, or marked to be computed (src/duck/functions.ts keptJsonValue) — keeps
|
|
217
|
+
* the document: `CASE WHEN <col> IS NULL OR <col> = KEPT_RECOMPUTE THEN <the call> ELSE <from the column> END`.
|
|
218
|
+
* null: not such a read, or not stored — the caller writes the call.
|
|
219
|
+
*/
|
|
220
|
+
keptRead(l, steps, either, canonical, asText) {
|
|
221
|
+
if (this.kept === null || kindOf(l) !== "ColumnRef")
|
|
222
|
+
return null;
|
|
223
|
+
const path = constString(steps);
|
|
224
|
+
const fields = (bodyOf(l).fields ?? []);
|
|
225
|
+
const last = fields.at(-1);
|
|
226
|
+
const t = this.typed(l);
|
|
227
|
+
if (path === null || last === undefined || kindOf(last) !== "String" || t === null || t.node !== "VAR" || t.relation === undefined || t.renamed !== null)
|
|
228
|
+
return null;
|
|
229
|
+
const at = this.kept.at(t.relation.relid, t.relation.attno);
|
|
230
|
+
if (at === null)
|
|
231
|
+
return null;
|
|
232
|
+
const read = canonicalRead({ source: at.source, column: at.column, steps: path, either, canonical, asText });
|
|
233
|
+
this.jsonReads.set(readKey(read), read);
|
|
234
|
+
const ready = this.kept.ready(read);
|
|
235
|
+
if (ready === null)
|
|
236
|
+
return null;
|
|
237
|
+
// the column as the statement names it (its FROM item's alias or table, as rule() qualifies a bare one)
|
|
238
|
+
const prefix = fields.length === 1 && t.qualifier !== null ? [S(t.qualifier)] : fields.slice(0, -1).map((f) => structuredClone(f));
|
|
239
|
+
const column = () => ({ ColumnRef: { fields: [...prefix.map((f) => structuredClone(f)), S(ready.name)] } });
|
|
240
|
+
const nul = () => fn("chr", [intConst(0)]);
|
|
241
|
+
const value = asText ? keptText(column, canonical) : caseWhen([[op("=", column(), nul()), nullConst()]], column());
|
|
242
|
+
if (ready.exact)
|
|
243
|
+
return value;
|
|
244
|
+
// (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(), op("||", nul(), strConst("!")))]), jsonGet(this.cp, l, steps, either, canonical, asText, canonical)]], value);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A JSON READ OFF `base` BY `steps`: from its stored column where every row holds it (keptRead), else DuckDB's own
|
|
249
|
+
* read where that is Postgres's answer, else the function (jsonGet). A jsonb's text holds each key once only where it
|
|
250
|
+
* is a COPIED TABLE'S column (the copy holds Postgres's canonical text; Typed.relation, as the stored paths find a
|
|
251
|
+
* column's table): a jsonb the statement computes — jsonb_object_agg, jsonb_build_object, in a CTE, a subquery or an
|
|
252
|
+
* arm of a UNION — is written by the rewrite as text that may hold a key twice, whose `->>` in Postgres is the LAST
|
|
253
|
+
* value, so its read checks every object on the way as a json column's does (a review finding).
|
|
254
|
+
*/
|
|
255
|
+
jsonRead(base, steps, either, canonical, asText) {
|
|
256
|
+
const t = kindOf(base) === "ColumnRef" ? this.typed(base) : null;
|
|
257
|
+
const copied = t !== null && t.node === "VAR" && t.relation !== undefined;
|
|
258
|
+
return this.keptRead(base, steps, either, canonical, asText) ?? jsonGet(this.cp, base, steps, either, canonical, asText, canonical && copied);
|
|
259
|
+
}
|
|
260
|
+
isCitext(oid) { return oid !== null && this.userTypes.get(oid) === "citext"; }
|
|
261
|
+
/** a text lower-cased by the database's case rules (what Postgres's citext compares, sorts and groups: str_tolower
|
|
262
|
+
* under the default collation) */
|
|
263
|
+
lowerOf(n) {
|
|
264
|
+
return this.caseMode === "libc" ? fn("lower", [n]) : fn("__mason_case", [n, boolConst(false), strConst(this.caseMode)]);
|
|
265
|
+
}
|
|
266
|
+
/** a citext value as the text citext compares: lower-cased, typed as text under the database's collation, so the
|
|
267
|
+
* text rules (the collation's sort keys, LIKE's escape) apply to it */
|
|
268
|
+
foldCitext(n) {
|
|
269
|
+
const out = this.lowerOf(n);
|
|
270
|
+
this.synthetic.set(bodyOf(out), { node: "FUNCEXPR", type: TEXT, typmod: -1, args: [], oid: null, format: null, collation: 100, inputCollation: 100, levelsUp: null, datum: null, qualifier: null, renamed: null });
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
/** the nodes the rewrite made whose type it decided itself (a citext lower-cased to text) */
|
|
274
|
+
synthetic = new WeakMap();
|
|
275
|
+
/** every CTE the statement declares, by name, from the queries walked so far (a subquery may read an outer one's) */
|
|
276
|
+
ctes = new Map();
|
|
277
|
+
/** What Postgres made of this raw node: its entry at the node's offset, of a kind the raw kind is analysed into. */
|
|
278
|
+
typed(n) {
|
|
279
|
+
const own = this.typedAt(n);
|
|
280
|
+
if (own !== null)
|
|
281
|
+
return own;
|
|
282
|
+
// an ORDER BY or a window's ORDER BY that repeats an expression of the SELECT list is analysed as THAT target
|
|
283
|
+
// (findTargetlistEntrySQL99): its own offset is in no analysed node, the same expression's elsewhere is
|
|
284
|
+
return this.byShape.get(shapeOf(n)) ?? null;
|
|
285
|
+
}
|
|
286
|
+
/** the nodes the rewrite made that Postgres analysed as another: typed as that one */
|
|
287
|
+
typedLike = new WeakMap();
|
|
288
|
+
/** what `widen` made, and the type it made it: a rule after it reads that type (a date widened to a timestamptz is
|
|
289
|
+
* one, never cast again), and a second pass leaves it as it is — a SELECT's expression and its GROUP BY twin must
|
|
290
|
+
* print alike, or DuckDB refuses the grouping (42803) */
|
|
291
|
+
widened = new WeakMap();
|
|
292
|
+
/** a literal Postgres analysed only where the statement repeats it (src/duck/rewrite.ts `typeCast`) */
|
|
293
|
+
constLike = new WeakMap();
|
|
294
|
+
/** the analysed type of each expression's shape (its tree less offsets), from the first place it was analysed */
|
|
295
|
+
byShape = new Map();
|
|
296
|
+
/** after `adopt`: every expression of the statement whose offset Postgres analysed, by its shape */
|
|
297
|
+
indexShapes(v) {
|
|
298
|
+
if (Array.isArray(v)) {
|
|
299
|
+
for (const x of v)
|
|
300
|
+
this.indexShapes(x);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (v === null || typeof v !== "object")
|
|
304
|
+
return;
|
|
305
|
+
if (isNode(v)) {
|
|
306
|
+
const t = this.typedAt(v);
|
|
307
|
+
if (t !== null) {
|
|
308
|
+
const k = shapeOf(v);
|
|
309
|
+
if (!this.byShape.has(k))
|
|
310
|
+
this.byShape.set(k, t);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const x of Object.values(isNode(v) ? bodyOf(v) : v))
|
|
314
|
+
this.indexShapes(x);
|
|
315
|
+
}
|
|
316
|
+
typedAt(n) {
|
|
317
|
+
const k = kindOf(n);
|
|
318
|
+
const b = bodyOf(n);
|
|
319
|
+
const own = this.synthetic.get(b);
|
|
320
|
+
if (own !== undefined)
|
|
321
|
+
return own;
|
|
322
|
+
if (k === "TypeCast") {
|
|
323
|
+
// a cast to the type the value already has leaves no node of its own: the value's
|
|
324
|
+
return this.at(b, ANALYSED.TypeCast) ?? (b.arg !== undefined ? this.typedAt(b.arg) : null);
|
|
325
|
+
}
|
|
326
|
+
if (k === "CollateClause")
|
|
327
|
+
return b.arg !== undefined ? this.typedAt(b.arg) : null;
|
|
328
|
+
// a node the rewrite made that reads as another (decidedByKey's any_value, a set operation's sort column)
|
|
329
|
+
const like = this.typedLike.get(b);
|
|
330
|
+
if (like !== undefined)
|
|
331
|
+
return this.typed(like);
|
|
332
|
+
const kinds = ANALYSED[k];
|
|
333
|
+
return kinds === undefined ? null : this.at(b, kinds);
|
|
334
|
+
}
|
|
335
|
+
at(b, kinds) {
|
|
336
|
+
if (typeof b.location !== "number" || b.location < 0)
|
|
337
|
+
return null;
|
|
338
|
+
return this.types.get(b.location - this.base)?.find((t) => kinds.includes(t.node)) ?? null;
|
|
339
|
+
}
|
|
340
|
+
typeOf(n) { return this.widened.get(bodyOf(n)) ?? this.typed(n)?.type ?? null; }
|
|
341
|
+
// ------------------------------------------------------------------------------------------------ the walk
|
|
342
|
+
/** marks every body of the statement as its own */
|
|
343
|
+
adopt(v) {
|
|
344
|
+
if (Array.isArray(v)) {
|
|
345
|
+
for (const x of v)
|
|
346
|
+
this.adopt(x);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (v === null || typeof v !== "object")
|
|
350
|
+
return;
|
|
351
|
+
if (isNode(v))
|
|
352
|
+
this.original.add(bodyOf(v));
|
|
353
|
+
for (const [k, x] of Object.entries(isNode(v) ? bodyOf(v) : v))
|
|
354
|
+
this.adopt(armOf(k, x) ?? x);
|
|
355
|
+
}
|
|
356
|
+
/** `clone` for the helpers outside the class */
|
|
357
|
+
cp = (n) => this.clone(n);
|
|
358
|
+
/** a deep copy that stays the statement's own (a rule that uses a node twice rewrites each use) */
|
|
359
|
+
clone(v) {
|
|
360
|
+
if (Array.isArray(v))
|
|
361
|
+
return v.map((x) => this.clone(x));
|
|
362
|
+
if (v === null || typeof v !== "object")
|
|
363
|
+
return v;
|
|
364
|
+
const out = {};
|
|
365
|
+
for (const [k, x] of Object.entries(v))
|
|
366
|
+
out[k] = this.clone(x);
|
|
367
|
+
if (isNode(v) && this.original.has(bodyOf(v)))
|
|
368
|
+
this.original.add(bodyOf(out));
|
|
369
|
+
// a node the rewrite typed itself stays typed (a picked operand of min / max, exactOf)
|
|
370
|
+
const own = isNode(v) ? this.synthetic.get(bodyOf(v)) : undefined;
|
|
371
|
+
if (own !== undefined)
|
|
372
|
+
this.synthetic.set(bodyOf(out), own);
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
walk(v) {
|
|
376
|
+
if (Array.isArray(v)) {
|
|
377
|
+
const out = [];
|
|
378
|
+
for (const x of v) {
|
|
379
|
+
const w = this.walk(x);
|
|
380
|
+
// a rule may answer several nodes for one (a sort key is several under a libc collation)
|
|
381
|
+
if (Array.isArray(w) && isNode(x))
|
|
382
|
+
out.push(...w);
|
|
383
|
+
else
|
|
384
|
+
out.push(w);
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
if (v === null || typeof v !== "object")
|
|
389
|
+
return v;
|
|
390
|
+
if (isNode(v)) {
|
|
391
|
+
const k = kindOf(v);
|
|
392
|
+
const b = bodyOf(v);
|
|
393
|
+
if (this.original.has(b)) {
|
|
394
|
+
// ONE PLACE PER NODE: a body met again, outside its own rule's answer, is a node a rule placed twice — and the
|
|
395
|
+
// second place would skip its rule (qualified or renamed in its first place alone; a review finding).
|
|
396
|
+
// A rule that uses a node twice clones it per use (`clone`, `cp`)
|
|
397
|
+
if (this.placed.has(b) && !this.answering.has(b))
|
|
398
|
+
placedTwice(k, b);
|
|
399
|
+
this.placed.add(b);
|
|
400
|
+
}
|
|
401
|
+
if (this.original.has(b) && !this.done.has(b)) {
|
|
402
|
+
this.done.add(b);
|
|
403
|
+
const r = this.rule(k, b);
|
|
404
|
+
if (r !== null) {
|
|
405
|
+
// the answer may hold the node itself (a rule wrapping its own node): that is the same place
|
|
406
|
+
this.answering.add(b);
|
|
407
|
+
try {
|
|
408
|
+
return Array.isArray(r) ? r.map((x) => this.walk(x)) : this.walk(r);
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
this.answering.delete(b);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { [k]: this.walkBody(b) };
|
|
416
|
+
}
|
|
417
|
+
return this.walkBody(v);
|
|
418
|
+
}
|
|
419
|
+
walkBody(b) {
|
|
420
|
+
const out = {};
|
|
421
|
+
for (const [key, x] of Object.entries(b)) {
|
|
422
|
+
const arm = armOf(key, x);
|
|
423
|
+
if (arm === null) {
|
|
424
|
+
out[key] = this.walk(x);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
// a set operation's arm is a query of its own: every rule of a SELECT (its GROUP BY's decided columns, its ORDER
|
|
428
|
+
// BY's collation, its WITH TIES) applies to it, as to the statement
|
|
429
|
+
const w = this.walk(arm);
|
|
430
|
+
if (!isNode(w) || kindOf(w) !== "SelectStmt")
|
|
431
|
+
throw new Unrewritable("this arm of a set operation is not supported by Mason", "write the arm as a subquery: SELECT * FROM (…) AS a", "unverified");
|
|
432
|
+
out[key] = bodyOf(w);
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
/** a node's rewrite, or null to keep it (its children are still walked) */
|
|
437
|
+
rule(k, b) {
|
|
438
|
+
switch (k) {
|
|
439
|
+
case "SelectStmt": return this.select(b);
|
|
440
|
+
case "A_Expr": return this.aExpr(b);
|
|
441
|
+
case "FuncCall": {
|
|
442
|
+
const r = this.funcCall(b);
|
|
443
|
+
if (r === null && b.agg_within_group === true)
|
|
444
|
+
this.widenArgs(b);
|
|
445
|
+
return r;
|
|
446
|
+
}
|
|
447
|
+
case "TypeCast": return this.typeCast(b);
|
|
448
|
+
case "A_Const": return this.aConst(b);
|
|
449
|
+
case "ParamRef": return this.param(b);
|
|
450
|
+
case "SortBy": return this.sortBy(b, null, null);
|
|
451
|
+
case "RangeFunction": return this.rangeFunction(b);
|
|
452
|
+
case "CollateClause": return this.collateClause(b);
|
|
453
|
+
case "MinMaxExpr": return this.minMax(b);
|
|
454
|
+
case "SQLValueFunction": return this.sqlValueFunction(b);
|
|
455
|
+
// a row as a value anywhere but the SELECT list, a cast to text and the json functions (rowValues) is a struct
|
|
456
|
+
// to DuckDB, compared, tested and grouped by its rules
|
|
457
|
+
case "ColumnRef":
|
|
458
|
+
case "RowExpr": {
|
|
459
|
+
const t = this.typed({ [k]: b });
|
|
460
|
+
if (t !== null && (k === "RowExpr" ? t.node === "ROWEXPR" : t.node === "VAR" && this.isRowType(t.type))) {
|
|
461
|
+
throw new Unrewritable("a row used as a value here (compared, tested, grouped or sorted) is not supported by Mason", "compare, test or sort its columns", "row-value");
|
|
462
|
+
}
|
|
463
|
+
// a bare name another FROM item has in another case (`"Email"` beside `email`) is ambiguous to DuckDB, which
|
|
464
|
+
// binds names case-insensitively: it is qualified with the FROM item Postgres resolved it to
|
|
465
|
+
// and a column its subquery has twice in two cases (`"T"`, `t`) is that subquery's renamed one (caseTwins)
|
|
466
|
+
const fields = (b.fields ?? []);
|
|
467
|
+
const last = fields.at(-1);
|
|
468
|
+
if (k === "ColumnRef" && t?.node === "VAR" && last !== undefined && kindOf(last) === "String" && (t.renamed !== null || (t.qualifier !== null && fields.length === 1))) {
|
|
469
|
+
const prefix = fields.length === 1 && t.qualifier !== null ? [S(t.qualifier)] : fields.slice(0, -1);
|
|
470
|
+
return { ColumnRef: { ...b, fields: [...prefix, t.renamed !== null ? S(t.renamed) : last] } };
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
case "RangeSubselect":
|
|
475
|
+
case "RangeVar": {
|
|
476
|
+
// a subquery or a CTE whose columns have twins in another case: named by an alias list, the twins renamed
|
|
477
|
+
const alias = b.alias;
|
|
478
|
+
const name = alias?.aliasname ?? (k === "RangeVar" ? b.relname : undefined);
|
|
479
|
+
const names = typeof name === "string" && alias?.colnames === undefined ? this.twins.get(name) : undefined;
|
|
480
|
+
if (names !== undefined)
|
|
481
|
+
b.alias = { ...(alias ?? {}), aliasname: name, colnames: names.map(S) };
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
default: return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* A ZONE AS POSTGRES READS IT (AT TIME ZONE, date_trunc's third argument): an IANA name in any case, spelled as tzdata
|
|
489
|
+
* spells it; `UT` and `Z` as UTC. Postgres reads an abbreviation or a POSIX zone with a fixed offset (`CST` is -06 all
|
|
490
|
+
* year, `UTC+3` three hours west) where DuckDB reads several as another zone (`CST` as Chicago's, `IST` as India's,
|
|
491
|
+
* `UTC+3` east), so those are refused (src/lib/timezone.ts atTimeZoneOf, the rule the other paths apply): a constant
|
|
492
|
+
* here, a row's value when it is read (`__mason_zone`).
|
|
493
|
+
*/
|
|
494
|
+
zoneArg(z) {
|
|
495
|
+
const text = constString(z);
|
|
496
|
+
if (text === null)
|
|
497
|
+
return fn("__mason_zone", [z]);
|
|
498
|
+
const read = atTimeZoneOf(text);
|
|
499
|
+
if ("problem" in read)
|
|
500
|
+
throw new Unrewritable(read.problem, "write an IANA zone name (America/Chicago, Asia/Kolkata)", read.sqlstate === "0A000" ? "zone-abbreviation" : { postgres: read.sqlstate });
|
|
501
|
+
return strConst(read.zone);
|
|
502
|
+
}
|
|
503
|
+
/** the parameters the session sends the transaction's start in (DuckPlan.clock), numbered after the statement's own */
|
|
504
|
+
clock = {};
|
|
505
|
+
/** the transaction's start as of the Parse (`parse`) or the execution (`exec`), a TIMESTAMPTZ parameter */
|
|
506
|
+
now(which) {
|
|
507
|
+
this.clock[which] ??= this.params.length + Object.keys(this.clock).length + 1;
|
|
508
|
+
return cast({ ParamRef: { number: this.clock[which] } }, ["timestamptz"]);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* A RELATIVE TIME LITERAL (`'now'`, `'today'`, `'tomorrow'`, `'yesterday'`): Postgres reads it once, when it analyses
|
|
512
|
+
* the statement — the transaction's start, and its day in the session's zone — and a prepared statement keeps that
|
|
513
|
+
* value. So it is the Parse's start (a parameter the session sends), never DuckDB's clock or the plan's.
|
|
514
|
+
*/
|
|
515
|
+
relativeTime(word, type) {
|
|
516
|
+
const days = { now: 0, today: 0, tomorrow: 1, yesterday: -1 }[word];
|
|
517
|
+
const zone = strConst(this.timeZone);
|
|
518
|
+
const local = fn("timezone", [zone, this.now("parse")]);
|
|
519
|
+
const day = () => { const d = cast(fn("timezone", [strConst(this.timeZone), this.now("parse")]), ["date"]); return days === 0 ? d : op("+", d, intConst(days)); };
|
|
520
|
+
if (type === DATE)
|
|
521
|
+
return day();
|
|
522
|
+
if (type === TIMESTAMP)
|
|
523
|
+
return word === "now" ? local : cast(day(), ["timestamp"]);
|
|
524
|
+
return word === "now" ? this.now("parse") : fn("timezone", [zone, cast(day(), ["timestamp"])]);
|
|
525
|
+
}
|
|
526
|
+
/** CURRENT_DATE and its kin: the transaction's start as of the execution, in the session's zone (DuckDB's own clock
|
|
527
|
+
* starts with each statement, and a transaction of several would read several) */
|
|
528
|
+
sqlValueFunction(b) {
|
|
529
|
+
const exec = () => this.now("exec");
|
|
530
|
+
switch (b.op) {
|
|
531
|
+
case "SVFOP_CURRENT_TIMESTAMP": return exec();
|
|
532
|
+
case "SVFOP_CURRENT_DATE": return cast(exec(), ["date"]);
|
|
533
|
+
case "SVFOP_LOCALTIMESTAMP": return cast(exec(), ["timestamp"]);
|
|
534
|
+
case "SVFOP_LOCALTIME": return cast(cast(exec(), ["timestamp"]), ["time"]);
|
|
535
|
+
default: return null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
isRowType(oid) { return oid === RECORD || this.userTypes.get(oid)?.startsWith("rowtype:") === true; }
|
|
539
|
+
/** a typed reference to a FROM item's column (a row's, read as its columns) */
|
|
540
|
+
typedRef(names, type, typmod) {
|
|
541
|
+
const n = { ColumnRef: { fields: names.map(S) } };
|
|
542
|
+
this.synthetic.set(bodyOf(n), { node: "VAR", type, typmod, args: [], oid: null, format: null, collation: TEXTS.has(type) ? 100 : 0, inputCollation: 0, levelsUp: 0, datum: null, qualifier: null, renamed: null });
|
|
543
|
+
return n;
|
|
544
|
+
}
|
|
545
|
+
/** a whole row's columns as values, each typed: a FROM item's (`r`, `r.*`, a table's own name) or a ROW(…)'s; null
|
|
546
|
+
* where n is no row */
|
|
547
|
+
rowFields(n, select) {
|
|
548
|
+
const k = kindOf(n);
|
|
549
|
+
if (k === "RowExpr") {
|
|
550
|
+
if (this.typed(n)?.node !== "ROWEXPR")
|
|
551
|
+
return null;
|
|
552
|
+
return (bodyOf(n).args ?? []).map((a, i) => ({ name: `f${i + 1}`, value: a, type: this.typeOf(a) }));
|
|
553
|
+
}
|
|
554
|
+
if (k !== "ColumnRef")
|
|
555
|
+
return null;
|
|
556
|
+
const fields = (bodyOf(n).fields ?? []);
|
|
557
|
+
const star = fields.length === 2 && kindOf(fields[1]) === "A_Star";
|
|
558
|
+
if (fields.length !== 1 && !star)
|
|
559
|
+
return null;
|
|
560
|
+
const t = this.typed(n);
|
|
561
|
+
if (t === null || t.node !== "VAR" || !this.isRowType(t.type))
|
|
562
|
+
return null;
|
|
563
|
+
const alias = String(bodyOf(fields[0]).sval);
|
|
564
|
+
const table = this.rowTypes.get(t.type);
|
|
565
|
+
if (table !== undefined)
|
|
566
|
+
return table.map((c) => ({ name: c.name, value: this.typedRef([alias, c.name], c.type, c.typmod), type: c.type }));
|
|
567
|
+
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 copied the table in part`, "name its columns instead", "row-value");
|
|
569
|
+
const source = this.fromSources(select).filter((q) => q.alias === alias);
|
|
570
|
+
if (source.length !== 1)
|
|
571
|
+
throw new Unrewritable("a row of this FROM item is not supported by Mason", "name its columns instead", "row-value");
|
|
572
|
+
let q = bodyOf(source[0].query);
|
|
573
|
+
while (q.op !== undefined && q.op !== "SETOP_NONE" && q.larg !== undefined)
|
|
574
|
+
q = q.larg;
|
|
575
|
+
const targets = (q.targetList ?? []);
|
|
576
|
+
if (targets.some((x) => hasStar(bodyOf(x).val)))
|
|
577
|
+
throw new Unrewritable("a row of a subquery that selects * is not supported by Mason", "name the subquery's columns", "row-value");
|
|
578
|
+
return targets.map((x) => {
|
|
579
|
+
const val = bodyOf(x).val;
|
|
580
|
+
const name = bodyOf(x).name ?? figureColname(val);
|
|
581
|
+
const typed = this.typed(val);
|
|
582
|
+
return { name, value: this.typedRef([alias, name], typed?.type ?? TEXT, typed?.typmod ?? -1), type: typed?.type ?? null };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
/** a row's text as record_out writes it: each column's text, quoted where it must be, NULL as nothing */
|
|
586
|
+
recordText(cols) {
|
|
587
|
+
for (const c of cols)
|
|
588
|
+
if (c.type !== null && this.isRowType(c.type))
|
|
589
|
+
throw new Unrewritable("a row holding a row is not supported by Mason", "name the inner row's columns", "row-value");
|
|
590
|
+
return fn("__mason_record_text", cols.map((c) => this.pgText(c.value, c.type, false) ?? cast(c.value, ["varchar"])));
|
|
591
|
+
}
|
|
592
|
+
/** a row's json as row_to_json writes it (`{"id":1,"t":"a"}`) */
|
|
593
|
+
rowJson(cols, fnName) {
|
|
594
|
+
const values = cols.map((c) => this.jsonValue(c.value, c.type, fnName));
|
|
595
|
+
return fn("__mason_json_build", [strConst(`r:${values.map((v) => `s${v.kind}`).join("")}`), ...cols.flatMap((c, i) => [strConst(c.name), values[i].value])]);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* A ROW AS A VALUE (the corpus run: `SELECT r FROM (…) r`, `row_to_json(t)`, `json_agg(t)`): DuckDB holds
|
|
599
|
+
* it as a struct, written `{'id': 1}`. In this query's own expressions (not its subqueries'), each row the SELECT list
|
|
600
|
+
* writes, casts to text or hands a json function is written as Postgres writes it, from its columns; any other row is
|
|
601
|
+
* refused by the ColumnRef / RowExpr rule.
|
|
602
|
+
*/
|
|
603
|
+
rowValues(b) {
|
|
604
|
+
const textTypes = new Set(["text", "varchar"]);
|
|
605
|
+
const visit = (v, inList) => {
|
|
606
|
+
if (Array.isArray(v))
|
|
607
|
+
return v.map((x) => visit(x, inList));
|
|
608
|
+
if (!isNode(v))
|
|
609
|
+
return v;
|
|
610
|
+
const k = kindOf(v), body = bodyOf(v);
|
|
611
|
+
if (k === "SelectStmt" || k === "SubLink" || k === "RangeSubselect")
|
|
612
|
+
return v;
|
|
613
|
+
if (inList) {
|
|
614
|
+
const cols = this.rowFields(v, b);
|
|
615
|
+
if (cols !== null)
|
|
616
|
+
return this.recordText(cols);
|
|
617
|
+
}
|
|
618
|
+
if (k === "TypeCast" && isNode(body.arg) && textTypes.has(lastName(body.typeName?.names))) {
|
|
619
|
+
const cols = this.rowFields(body.arg, b);
|
|
620
|
+
if (cols !== null)
|
|
621
|
+
return this.recordText(cols);
|
|
622
|
+
}
|
|
623
|
+
if (k === "FuncCall" && Array.isArray(body.args) && body.args.length >= 1 && body.over === undefined) {
|
|
624
|
+
const name = lastName(body.funcname);
|
|
625
|
+
const cols = ["to_json", "to_jsonb", "row_to_json", "json_agg", "jsonb_agg"].includes(name) ? this.rowFields(body.args[0], b) : null;
|
|
626
|
+
if (cols !== null) {
|
|
627
|
+
if (body.args.length > 1)
|
|
628
|
+
throw new Unrewritable(`${name}() of a row with more arguments is not supported by Mason`, `${name}(row)`, "row-value");
|
|
629
|
+
const json = this.rowJson(cols, name);
|
|
630
|
+
if (name === "to_json" || name === "row_to_json")
|
|
631
|
+
return json;
|
|
632
|
+
if (name === "to_jsonb")
|
|
633
|
+
return cast(fn("__mason_jsonb_text", [json]), ["json"]);
|
|
634
|
+
if (body.agg_distinct === true)
|
|
635
|
+
throw new Unrewritable(`${name}(DISTINCT …) is not supported by Mason`, `select the DISTINCT values in a subquery and ${name} them`, "json-agg-distinct");
|
|
636
|
+
// json_agg puts a line break before each row but the first (json_agg_transfn: a composite's or an array's)
|
|
637
|
+
const array = linedAgg(body, concat(strConst("\n "), json), this.done);
|
|
638
|
+
return name === "jsonb_agg" ? cast(fn("__mason_jsonb_text", [array]), ["json"]) : array;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const out = {};
|
|
642
|
+
for (const [key, x] of Object.entries(body))
|
|
643
|
+
out[key] = visit(x, false);
|
|
644
|
+
// the node stays the statement's own (its rule still runs), with its children replaced in place
|
|
645
|
+
for (const [key, x] of Object.entries(out))
|
|
646
|
+
body[key] = x;
|
|
647
|
+
return v;
|
|
648
|
+
};
|
|
649
|
+
if (Array.isArray(b.targetList))
|
|
650
|
+
for (const t of b.targetList) {
|
|
651
|
+
const tb = bodyOf(t);
|
|
652
|
+
if (isNode(tb.val))
|
|
653
|
+
tb.val = visit(tb.val, true);
|
|
654
|
+
}
|
|
655
|
+
for (const key of ["whereClause", "havingClause"])
|
|
656
|
+
if (isNode(b[key]))
|
|
657
|
+
b[key] = visit(b[key], false);
|
|
658
|
+
}
|
|
659
|
+
// ---------------------------------------------------------------------------------------- SELECT
|
|
660
|
+
select(b) {
|
|
661
|
+
for (const c of (b.withClause?.ctes ?? []))
|
|
662
|
+
if (isNode(c) && typeof bodyOf(c).ctename === "string" && isNode(bodyOf(c).ctequery))
|
|
663
|
+
this.ctes.set(bodyOf(c).ctename, bodyOf(c).ctequery);
|
|
664
|
+
this.rowValues(b);
|
|
665
|
+
// a GROUP BY of an output name is grouped by its ordinal: DuckDB binds a name without regard to case, so `"Name"`
|
|
666
|
+
// would be another table's `name` (the corpus run of fbbf8f08), where Postgres finds no input column of that name
|
|
667
|
+
if (Array.isArray(b.groupClause) && Array.isArray(b.targetList)) {
|
|
668
|
+
const targets = b.targetList;
|
|
669
|
+
b.groupClause = b.groupClause.map((g) => {
|
|
670
|
+
const f = kindOf(g) === "ColumnRef" ? names(bodyOf(g).fields) : [];
|
|
671
|
+
if (f.length !== 1 || this.typedAt(g)?.node === "VAR")
|
|
672
|
+
return g;
|
|
673
|
+
const at = targets.findIndex((t) => bodyOf(t).name === f[0]);
|
|
674
|
+
return at >= 0 && targets.filter((t) => bodyOf(t).name === f[0]).length === 1 ? intConst(at + 1) : g;
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
for (const g of (b.groupClause ?? []))
|
|
678
|
+
if (isNode(g) && kindOf(g) !== "GroupingSet")
|
|
679
|
+
this.comparable(this.groupedExpression(g, b), "GROUP BY");
|
|
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) {
|
|
685
|
+
let first = b;
|
|
686
|
+
while (first.larg !== undefined)
|
|
687
|
+
first = first.larg;
|
|
688
|
+
for (const t of (first.targetList ?? []))
|
|
689
|
+
if (isNode(bodyOf(t).val))
|
|
690
|
+
this.comparable(bodyOf(t).val, `${String(b.op).replace("SETOP_", "")} (without ALL) of`);
|
|
691
|
+
}
|
|
692
|
+
this.decidedByKey(b);
|
|
693
|
+
// the statement's own ORDER BY: an ordinal or an output name sorts by that target's expression, so its key is
|
|
694
|
+
// built over the expression (DuckDB's `ORDER BY 1 COLLATE …` would sort by the constant)
|
|
695
|
+
if (Array.isArray(b.sortClause) && Array.isArray(b.targetList) && (b.op === undefined || b.op === "SETOP_NONE")) {
|
|
696
|
+
const targets = b.targetList;
|
|
697
|
+
b.sortClause = b.sortClause.flatMap((s) => {
|
|
698
|
+
const sb = bodyOf(s);
|
|
699
|
+
this.done.add(sb);
|
|
700
|
+
return this.sortBy(sb, targets, b) ?? [s];
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (Array.isArray(b.sortClause) && b.op !== undefined && b.op !== "SETOP_NONE")
|
|
704
|
+
this.setOperationOrdinals(b);
|
|
705
|
+
if (b.limitOption === "LIMIT_OPTION_WITH_TIES")
|
|
706
|
+
return this.withTies(b);
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* A SET OPERATION'S ORDER BY of a text column (`… UNION … ORDER BY 1`, or `ORDER BY status` where the first SELECT
|
|
711
|
+
* says `o.status`): Postgres sorts by its output column, which it analysed at no offset of the ORDER BY's own, so the
|
|
712
|
+
* SortBy rule found no type and DuckDB sorted by bytes. Each is typed as the first SELECT's target, and an ordinal is
|
|
713
|
+
* sorted by that output column's name, so the rule builds the collation's keys. A target with no name of its own is
|
|
714
|
+
* given one: Postgres's analysis names the columns the tool reads, never DuckDB's.
|
|
715
|
+
*/
|
|
716
|
+
setOperationOrdinals(b) {
|
|
717
|
+
// a set operation's arms are SelectStmt bodies, unwrapped (libpg-query's larg / rarg)
|
|
718
|
+
let arm = b;
|
|
719
|
+
while (arm.op !== undefined && arm.op !== "SETOP_NONE" && arm.larg !== undefined)
|
|
720
|
+
arm = isNode(arm.larg) ? bodyOf(arm.larg) : arm.larg;
|
|
721
|
+
const targets = (arm.targetList ?? []);
|
|
722
|
+
const outputName = (t) => typeof t.name === "string" ? t.name
|
|
723
|
+
: isNode(t.val) && kindOf(t.val) === "ColumnRef" ? names(bodyOf(t.val).fields).at(-1) ?? null : null;
|
|
724
|
+
for (const s of b.sortClause) {
|
|
725
|
+
const sb = bodyOf(s);
|
|
726
|
+
const by = sb.node;
|
|
727
|
+
const f = kindOf(by) === "ColumnRef" ? names(bodyOf(by).fields) : [];
|
|
728
|
+
if (f.length === 1) {
|
|
729
|
+
const named = targets.map(bodyOf).filter((t) => outputName(t) === f[0]);
|
|
730
|
+
if (named.length === 1)
|
|
731
|
+
this.typedLike.set(bodyOf(by), named[0].val);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const n = constInt(by);
|
|
735
|
+
if (n === null || n < 1 || n > targets.length)
|
|
736
|
+
continue;
|
|
737
|
+
if (targets.slice(0, n).some((t) => hasStar(bodyOf(t).val))) {
|
|
738
|
+
throw new Unrewritable("ORDER BY a column number of a set operation whose first SELECT lists * is not supported by Mason", "ORDER BY the column's name", "setop-ordinal-star");
|
|
739
|
+
}
|
|
740
|
+
const target = bodyOf(targets[n - 1]);
|
|
741
|
+
const value = target.val;
|
|
742
|
+
const c = this.textCollation(value);
|
|
743
|
+
if (c === null || c.kind === "bytes")
|
|
744
|
+
continue;
|
|
745
|
+
if (target.name === undefined)
|
|
746
|
+
target.name = `__mason_sort_${n}`;
|
|
747
|
+
const ref = { ColumnRef: { fields: [S(String(target.name))] } };
|
|
748
|
+
this.typedLike.set(bodyOf(ref), value);
|
|
749
|
+
sb.node = ref;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* A COLUMN THE GROUP'S KEY DECIDES (`SELECT c.id, c.name … GROUP BY c.id`, c.id the primary key): Postgres accepts it
|
|
754
|
+
* ungrouped (functional dependency), DuckDB refuses it (42803). Postgres accepted this statement, so every column of
|
|
755
|
+
* this query outside an aggregate has one value per group: `any_value(column)` is that value, exactly — in the SELECT
|
|
756
|
+
* list, HAVING, ORDER BY, a window's OVER, and a correlated subquery (where this query's column is one level up:
|
|
757
|
+
* DuckDB, as SQL says, computes an aggregate of outer columns in the outer query). `t.*` is `any_value(COLUMNS(t.*))`.
|
|
758
|
+
* A group key itself is left as it is, and so are an aggregate's argument, a subquery's own columns, and a grouping
|
|
759
|
+
* set (where a key reads NULL in its total rows).
|
|
760
|
+
*/
|
|
761
|
+
decidedByKey(b) {
|
|
762
|
+
const groups = (b.groupClause ?? []);
|
|
763
|
+
if (groups.length === 0 || groups.some((g) => kindOf(g) === "GroupingSet"))
|
|
764
|
+
return;
|
|
765
|
+
const targets = (b.targetList ?? []);
|
|
766
|
+
// a target the GROUP BY names by its ordinal or its output name is a key itself, and DuckDB reads it there: untouched
|
|
767
|
+
const keyTargets = new Set();
|
|
768
|
+
for (const g of groups) {
|
|
769
|
+
const n = constInt(g);
|
|
770
|
+
if (n !== null && n >= 1 && n <= targets.length) {
|
|
771
|
+
keyTargets.add(n - 1);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
// a bare name is an output name only where it is no input column (Postgres prefers the column)
|
|
775
|
+
const f = kindOf(g) === "ColumnRef" ? names(bodyOf(g).fields) : [];
|
|
776
|
+
if (f.length === 1 && this.typedAt(g)?.node !== "VAR")
|
|
777
|
+
targets.forEach((t, i) => { if (bodyOf(t).name === f[0])
|
|
778
|
+
keyTargets.add(i); });
|
|
779
|
+
}
|
|
780
|
+
const grouped = new Set([...groups.map(shapeOf), ...[...keyTargets].map((i) => shapeOf(bodyOf(targets[i]).val))]);
|
|
781
|
+
// `depth`: the subqueries between here and this query (a correlated subquery's column of this query is levelsUp 1)
|
|
782
|
+
const wrap = (v, depth) => {
|
|
783
|
+
if (Array.isArray(v))
|
|
784
|
+
return v.map((x) => wrap(x, depth));
|
|
785
|
+
if (v === null || typeof v !== "object")
|
|
786
|
+
return v;
|
|
787
|
+
// a plain body (a window's OVER, a SubLink's select): its parts
|
|
788
|
+
if (!isNode(v)) {
|
|
789
|
+
for (const [key, x] of Object.entries(v))
|
|
790
|
+
if (key !== "location")
|
|
791
|
+
v[key] = wrap(x, depth);
|
|
792
|
+
return v;
|
|
793
|
+
}
|
|
794
|
+
const k = kindOf(v), n = bodyOf(v);
|
|
795
|
+
if (grouped.has(shapeOf(v)))
|
|
796
|
+
return v;
|
|
797
|
+
if (k === "SubLink") {
|
|
798
|
+
for (const [key, x] of Object.entries(n))
|
|
799
|
+
if (key !== "location")
|
|
800
|
+
n[key] = wrap(x, depth + 1);
|
|
801
|
+
return v;
|
|
802
|
+
}
|
|
803
|
+
if (k === "FuncCall" && n.over === undefined && this.typed(v)?.node === "AGGREF")
|
|
804
|
+
return v;
|
|
805
|
+
if (k === "ColumnRef" && hasStar(v)) {
|
|
806
|
+
if (depth > 0)
|
|
807
|
+
return v;
|
|
808
|
+
// `c.*` beside an aggregate: every column of c, each through any_value (DuckDB's COLUMNS expands the star)
|
|
809
|
+
const call = { FuncCall: { funcname: [S("any_value")], args: [{ FuncCall: { funcname: [S("columns")], args: [v], funcformat: "COERCE_EXPLICIT_CALL" } }], funcformat: "COERCE_EXPLICIT_CALL" } };
|
|
810
|
+
this.done.add(bodyOf(call));
|
|
811
|
+
return call;
|
|
812
|
+
}
|
|
813
|
+
if (k === "ColumnRef") {
|
|
814
|
+
// an output name (`ORDER BY p` of `max(x) AS p`) is the target, no column: its shape may be another query's
|
|
815
|
+
// column (the corpus run of fbbf8f08: `… GROUP BY p` outside read it as one, and any_value(p) nested aggregates)
|
|
816
|
+
const f = names(n.fields);
|
|
817
|
+
if (depth === 0 && f.length === 1 && this.typedAt(v) === null && targets.some((t) => bodyOf(t).name === f[0]))
|
|
818
|
+
return v;
|
|
819
|
+
const t = this.typed(v);
|
|
820
|
+
if (t?.node !== "VAR" || t.levelsUp !== depth)
|
|
821
|
+
return v;
|
|
822
|
+
const call = { FuncCall: { funcname: [S("any_value")], args: [v], funcformat: "COERCE_EXPLICIT_CALL" } };
|
|
823
|
+
this.done.add(bodyOf(call));
|
|
824
|
+
// typed as the column it reads: a sort's collation, a cast's source type
|
|
825
|
+
this.typedLike.set(bodyOf(call), v);
|
|
826
|
+
return call;
|
|
827
|
+
}
|
|
828
|
+
for (const [key, x] of Object.entries(n))
|
|
829
|
+
if (key !== "location")
|
|
830
|
+
n[key] = wrap(x, depth);
|
|
831
|
+
return v;
|
|
832
|
+
};
|
|
833
|
+
targets.forEach((t, i) => { if (!keyTargets.has(i)) {
|
|
834
|
+
const tb = bodyOf(t);
|
|
835
|
+
tb.val = wrap(tb.val, 0);
|
|
836
|
+
} });
|
|
837
|
+
if (b.havingClause !== undefined)
|
|
838
|
+
b.havingClause = wrap(b.havingClause, 0);
|
|
839
|
+
if (Array.isArray(b.sortClause))
|
|
840
|
+
for (const s of b.sortClause) {
|
|
841
|
+
const sb = bodyOf(s);
|
|
842
|
+
sb.node = wrap(sb.node, 0);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* `FETCH FIRST n ROWS WITH TIES` (DuckDB has none): the rows whose rank under the ORDER BY is at most n — the
|
|
847
|
+
* statement, with that rank as a column of its own, read by a statement around it that keeps those rows in the
|
|
848
|
+
* same order and leaves the rank out (`* EXCLUDE`, DuckDB's).
|
|
849
|
+
*/
|
|
850
|
+
withTies(b) {
|
|
851
|
+
if (b.distinctClause !== undefined)
|
|
852
|
+
throw new Unrewritable("FETCH … WITH TIES over a SELECT DISTINCT is not supported by Mason (DuckDB has no WITH TIES)", "select the DISTINCT rows in a subquery and FETCH … WITH TIES from it", "with-ties-distinct");
|
|
853
|
+
const order = b.sortClause ?? [];
|
|
854
|
+
const count = b.limitCount;
|
|
855
|
+
delete b.limitCount;
|
|
856
|
+
b.limitOption = "LIMIT_OPTION_DEFAULT";
|
|
857
|
+
const offset = b.limitOffset;
|
|
858
|
+
delete b.limitOffset;
|
|
859
|
+
const rankName = "__mason_rank";
|
|
860
|
+
const sortCopy = order.map((o) => this.clone(o));
|
|
861
|
+
for (const s of sortCopy)
|
|
862
|
+
this.done.add(bodyOf(s));
|
|
863
|
+
const rank = { FuncCall: { funcname: [S("rank")], over: { orderClause: sortCopy, frameOptions: 1058 }, funcformat: "COERCE_EXPLICIT_CALL" } };
|
|
864
|
+
this.done.add(bodyOf(rank));
|
|
865
|
+
b.targetList = [...b.targetList, { ResTarget: { name: rankName, val: rank } }];
|
|
866
|
+
const inner = { SelectStmt: b };
|
|
867
|
+
// the rank of the first row past the offset is offset + 1; a row is kept while its rank is within offset + n
|
|
868
|
+
const bound = offset === undefined ? count : op("+", offset, count);
|
|
869
|
+
const outer = {
|
|
870
|
+
targetList: [{ ResTarget: { val: { ColumnRef: { fields: [{ A_Star: {} }] } } } }],
|
|
871
|
+
fromClause: [{ RangeSubselect: { subquery: inner, alias: { aliasname: "__mason_ties" } } }],
|
|
872
|
+
whereClause: op("<=", { ColumnRef: { fields: [S(rankName)] } }, bound),
|
|
873
|
+
sortClause: [{ SortBy: { node: { ColumnRef: { fields: [S(rankName)] } }, sortby_dir: "SORTBY_DEFAULT", sortby_nulls: "SORTBY_NULLS_DEFAULT" } }],
|
|
874
|
+
op: "SETOP_NONE",
|
|
875
|
+
};
|
|
876
|
+
if (offset !== undefined)
|
|
877
|
+
outer.limitOffset = offset;
|
|
878
|
+
this.done.add(outer);
|
|
879
|
+
this.excludeRank = true;
|
|
880
|
+
return { SelectStmt: outer };
|
|
881
|
+
}
|
|
882
|
+
/** the outer WITH TIES statement reads `*` and must not send its rank: the server drops that last column */
|
|
883
|
+
excludeRank = false;
|
|
884
|
+
// ------------------------------------------------------------------------------------- exact numerics
|
|
885
|
+
/** The statement's numeric result columns as programs, their cells appended to its SELECT list (before the walk,
|
|
886
|
+
* which rewrites each cell as it rewrites the column). Only a plain SELECT: a UNION or a DISTINCT would read the
|
|
887
|
+
* hidden cells as part of each row. */
|
|
888
|
+
exactColumns(stmt, fields) {
|
|
889
|
+
if (kindOf(stmt) !== "SelectStmt" || fields === null)
|
|
890
|
+
return [];
|
|
891
|
+
const b = bodyOf(stmt);
|
|
892
|
+
if ((b.op !== undefined && b.op !== "SETOP_NONE") || b.distinctClause !== undefined || b.limitOption === "LIMIT_OPTION_WITH_TIES")
|
|
893
|
+
return [];
|
|
894
|
+
const targets = (b.targetList ?? []);
|
|
895
|
+
if (targets.length !== fields.length || targets.some((t) => hasStar(bodyOf(t).val)))
|
|
896
|
+
return [];
|
|
897
|
+
const grouped = (Array.isArray(b.groupClause) && b.groupClause.length > 0) || b.havingClause !== undefined || targets.some((t) => this.hasAggregate(bodyOf(t).val));
|
|
898
|
+
const groupShapes = new Set((b.groupClause ?? []).map((g) => {
|
|
899
|
+
const n = constInt(g);
|
|
900
|
+
return shapeOf(n !== null && n >= 1 && n <= targets.length ? bodyOf(targets[n - 1]).val : g);
|
|
901
|
+
}));
|
|
902
|
+
const cells = [];
|
|
903
|
+
const out = [];
|
|
904
|
+
targets.forEach((target, i) => {
|
|
905
|
+
const field = fields[i];
|
|
906
|
+
let node = bodyOf(target).val;
|
|
907
|
+
let text = false;
|
|
908
|
+
// a numeric's text (a varchar(n) cuts it, which the program does not: left to the rewrite's cast)
|
|
909
|
+
if (TEXTS.has(field.oid) && (field.typmod ?? -1) < 0 && kindOf(node) === "TypeCast" && this.typeOf(bodyOf(node).arg) === NUMERIC) {
|
|
910
|
+
node = bodyOf(node).arg;
|
|
911
|
+
text = true;
|
|
912
|
+
}
|
|
913
|
+
else if (field.oid !== NUMERIC)
|
|
914
|
+
return;
|
|
915
|
+
const mine = [];
|
|
916
|
+
const cell = (n, conv = "exact") => {
|
|
917
|
+
if (grouped && !this.validGrouped(n, groupShapes))
|
|
918
|
+
throw NOT_EXACT;
|
|
919
|
+
const shape = `${conv}:${shapeOf(n)}`;
|
|
920
|
+
let at = mine.findIndex((c) => c.shape === shape);
|
|
921
|
+
if (at < 0) {
|
|
922
|
+
mine.push({ node: n, conv, shape });
|
|
923
|
+
at = mine.length - 1;
|
|
924
|
+
}
|
|
925
|
+
return { op: "cell", key: String(at) };
|
|
926
|
+
};
|
|
927
|
+
let built;
|
|
928
|
+
try {
|
|
929
|
+
built = this.exactOf(node, cell);
|
|
930
|
+
}
|
|
931
|
+
catch (e) {
|
|
932
|
+
if (e === NOT_EXACT)
|
|
933
|
+
return;
|
|
934
|
+
throw e;
|
|
935
|
+
}
|
|
936
|
+
if (built === null || !worthComputing(built.p) && mine.every((c) => c.conv === "exact"))
|
|
937
|
+
return;
|
|
938
|
+
// a numeric(p, s) column as it is: the wire pads it to s (src/duck/types.ts pgTypedColumn), no program needed
|
|
939
|
+
if (built.p.op === "pad" && built.p.arg.op === "cell" && !text && (field.typmod ?? -1) >= 4 && ((field.typmod - 4) & 0xffff) === built.p.scale)
|
|
940
|
+
return;
|
|
941
|
+
// the column's cells after every earlier column's
|
|
942
|
+
const base = cells.length;
|
|
943
|
+
cells.push(...mine);
|
|
944
|
+
out.push({ column: i, program: offsetCells(built.p, base), cells: [], text });
|
|
945
|
+
});
|
|
946
|
+
if (out.length === 0)
|
|
947
|
+
return [];
|
|
948
|
+
for (const c of out)
|
|
949
|
+
c.cells = cells.map((x) => x.conv);
|
|
950
|
+
b.targetList = [...targets, ...cells.map((c, j) => ({ ResTarget: { name: `__mason_cell_${j}`, val: this.clone(c.node) } }))];
|
|
951
|
+
return out;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* The exact columns of a statement whose outermost query PASSES THROUGH what an inner one computed: `SELECT * FROM
|
|
955
|
+
* (inner) s`, a CTE read whole, the columns of one subquery by name (Metabase's nested question), a UNION ALL whose
|
|
956
|
+
* every arm builds the same programs. The inner query selects its cells (`exactColumns`), the outer selects them
|
|
957
|
+
* after its own columns, and the programs are the inner's at the outer's column. Anything else that computes over an
|
|
958
|
+
* inner exact numeric reads DuckDB's value (docs/SQL-SUPPORT.md `nested-exact-numeric`).
|
|
959
|
+
*/
|
|
960
|
+
exactThrough(stmt, fields) {
|
|
961
|
+
if (fields === null || kindOf(stmt) !== "SelectStmt")
|
|
962
|
+
return [];
|
|
963
|
+
// the query's CTEs are visible to every query inside it (`WITH base AS (…) SELECT b.x FROM (SELECT * FROM base) b`)
|
|
964
|
+
const ctes = bodyOf(stmt).withClause?.recursive === true ? [] : (bodyOf(stmt).withClause?.ctes ?? []);
|
|
965
|
+
this.cteScope.push(ctes);
|
|
966
|
+
try {
|
|
967
|
+
return this.exactThroughIn(stmt, fields);
|
|
968
|
+
}
|
|
969
|
+
finally {
|
|
970
|
+
this.cteScope.pop();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
/** the CTEs of the queries `exactThrough` is inside, outermost first (a recursive WITH's are none: never read whole) */
|
|
974
|
+
cteScope = [];
|
|
975
|
+
exactThroughIn(stmt, fields) {
|
|
976
|
+
const b = bodyOf(stmt);
|
|
977
|
+
if (b.op === "SETOP_UNION" && b.all === true)
|
|
978
|
+
return this.exactArms(b, fields);
|
|
979
|
+
const direct = this.exactColumns(stmt, fields);
|
|
980
|
+
if (direct.length > 0)
|
|
981
|
+
return direct;
|
|
982
|
+
const through = this.passThrough(b);
|
|
983
|
+
if (through === null)
|
|
984
|
+
return [];
|
|
985
|
+
const inner = this.exactThrough(through.query, this.outputFields(through.query));
|
|
986
|
+
if (inner.length === 0)
|
|
987
|
+
return [];
|
|
988
|
+
const cells = inner[0].cells.length;
|
|
989
|
+
if (through.columns !== "star") {
|
|
990
|
+
const targets = (b.targetList ?? []);
|
|
991
|
+
b.targetList = [...targets, ...Array.from({ length: cells }, (_, j) => ({ ResTarget: { name: `__mason_cell_${j}`,
|
|
992
|
+
val: { ColumnRef: { fields: [S(through.alias), S(`__mason_cell_${j}`)] } } } }))];
|
|
993
|
+
}
|
|
994
|
+
const map = through.columns;
|
|
995
|
+
return inner.flatMap((e) => (map === "star" ? [e.column] : map.flatMap((k, i) => k === e.column ? [i] : []))
|
|
996
|
+
.filter((i) => i < fields.length && fields[i].oid === NUMERIC || e.text).map((column) => ({ ...e, column })));
|
|
997
|
+
}
|
|
998
|
+
/** a UNION ALL's arms, each with the same programs at the same columns (else none is computed) */
|
|
999
|
+
exactArms(b, fields) {
|
|
1000
|
+
const arms = [];
|
|
1001
|
+
const flatten = (n) => {
|
|
1002
|
+
const x = bodyOf(n);
|
|
1003
|
+
if (x.op === "SETOP_UNION" && x.all === true && x.larg !== undefined && x.rarg !== undefined) {
|
|
1004
|
+
flatten({ SelectStmt: x.larg });
|
|
1005
|
+
flatten({ SelectStmt: x.rarg });
|
|
1006
|
+
}
|
|
1007
|
+
else
|
|
1008
|
+
arms.push(n);
|
|
1009
|
+
};
|
|
1010
|
+
flatten({ SelectStmt: b });
|
|
1011
|
+
// an arm typed otherwise than the union (an integer arm of a numeric column) is cast by Postgres: not this program
|
|
1012
|
+
if (arms.some((a) => { const f = this.outputFields(a); return f === null || f.some((x, i) => x.oid !== fields[i]?.oid); }))
|
|
1013
|
+
return [];
|
|
1014
|
+
const trial = arms.map((a) => this.exactThrough(this.clone(a), fields));
|
|
1015
|
+
const first = JSON.stringify(trial[0]);
|
|
1016
|
+
if (trial[0].length === 0 || trial.some((t) => JSON.stringify(t) !== first))
|
|
1017
|
+
return [];
|
|
1018
|
+
const made = arms.map((a) => this.exactThrough(a, fields));
|
|
1019
|
+
return made[0];
|
|
1020
|
+
}
|
|
1021
|
+
/** an outer query that only reads one subquery's or CTE's columns: the inner query, its alias, and which inner
|
|
1022
|
+
* column each outer one is ("star": all of them, in order) */
|
|
1023
|
+
passThrough(b) {
|
|
1024
|
+
if ((b.op !== undefined && b.op !== "SETOP_NONE") || b.groupClause !== undefined || b.havingClause !== undefined || b.distinctClause !== undefined || b.windowClause !== undefined)
|
|
1025
|
+
return null;
|
|
1026
|
+
const from = (b.fromClause ?? []);
|
|
1027
|
+
if (from.length !== 1)
|
|
1028
|
+
return null;
|
|
1029
|
+
const f = from[0];
|
|
1030
|
+
let query, alias;
|
|
1031
|
+
if (kindOf(f) === "RangeSubselect") {
|
|
1032
|
+
query = bodyOf(f).subquery;
|
|
1033
|
+
alias = bodyOf(f).alias?.aliasname;
|
|
1034
|
+
}
|
|
1035
|
+
else if (kindOf(f) === "RangeVar" && bodyOf(f).schemaname === undefined) {
|
|
1036
|
+
// its own WITH first, then the enclosing queries', innermost first (a nearer CTE of the name shadows a farther)
|
|
1037
|
+
if (b.withClause?.recursive === true)
|
|
1038
|
+
return null;
|
|
1039
|
+
const scopes = [(b.withClause?.ctes ?? []), ...[...this.cteScope].reverse()];
|
|
1040
|
+
const cte = scopes.map((list) => list.find((c) => bodyOf(c).ctename === bodyOf(f).relname)).find((c) => c !== undefined);
|
|
1041
|
+
if (cte === undefined || bodyOf(cte).aliascolnames !== undefined)
|
|
1042
|
+
return null;
|
|
1043
|
+
query = bodyOf(cte).ctequery;
|
|
1044
|
+
alias = bodyOf(f).alias?.aliasname ?? bodyOf(f).relname;
|
|
1045
|
+
}
|
|
1046
|
+
if (query === undefined || alias === undefined || kindOf(query) !== "SelectStmt")
|
|
1047
|
+
return null;
|
|
1048
|
+
if (bodyOf(f).alias?.colnames !== undefined)
|
|
1049
|
+
return null;
|
|
1050
|
+
const targets = (b.targetList ?? []);
|
|
1051
|
+
if (targets.some((t) => this.hasAggregate(bodyOf(t).val)))
|
|
1052
|
+
return null;
|
|
1053
|
+
if (targets.length === 1 && hasStar(bodyOf(targets[0]).val))
|
|
1054
|
+
return { query, alias, columns: "star" };
|
|
1055
|
+
const outputs = this.outputNames(query);
|
|
1056
|
+
if (outputs === null)
|
|
1057
|
+
return null;
|
|
1058
|
+
const columns = targets.map((t) => {
|
|
1059
|
+
const v = bodyOf(t).val;
|
|
1060
|
+
if (kindOf(v) !== "ColumnRef" || hasStar(v))
|
|
1061
|
+
return -1;
|
|
1062
|
+
const f = names(bodyOf(v).fields);
|
|
1063
|
+
const at = outputs.indexOf(f[f.length - 1]);
|
|
1064
|
+
return at >= 0 && outputs.lastIndexOf(f[f.length - 1]) === at ? at : -1;
|
|
1065
|
+
});
|
|
1066
|
+
return { query, alias, columns };
|
|
1067
|
+
}
|
|
1068
|
+
/** the query a `SELECT * FROM (q) s` passes through whole, or null: its output names and types are q's (a BI tool's
|
|
1069
|
+
* `SELECT "sql".* FROM (…) AS "sql"` inside a query that names a column of it) */
|
|
1070
|
+
starOf(b) {
|
|
1071
|
+
const through = this.passThrough(b);
|
|
1072
|
+
return through !== null && through.columns === "star" ? through.query : null;
|
|
1073
|
+
}
|
|
1074
|
+
/** a query's output column names, as Postgres names them (a set operation's are its first arm's) */
|
|
1075
|
+
outputNames(q) {
|
|
1076
|
+
let b = bodyOf(q);
|
|
1077
|
+
while (b.op !== undefined && b.op !== "SETOP_NONE" && b.larg !== undefined)
|
|
1078
|
+
b = b.larg;
|
|
1079
|
+
const star = this.starOf(b);
|
|
1080
|
+
if (star !== null)
|
|
1081
|
+
return this.outputNames(star);
|
|
1082
|
+
const targets = (b.targetList ?? []);
|
|
1083
|
+
if (targets.some((t) => hasStar(bodyOf(t).val)))
|
|
1084
|
+
return null;
|
|
1085
|
+
return targets.map((t) => bodyOf(t).name ?? figureColname(bodyOf(t).val));
|
|
1086
|
+
}
|
|
1087
|
+
/** a query's output columns' types, as Postgres analysed them (a set operation's: its first arm's) */
|
|
1088
|
+
outputFields(q) {
|
|
1089
|
+
let b = bodyOf(q);
|
|
1090
|
+
while (b.op !== undefined && b.op !== "SETOP_NONE" && b.larg !== undefined)
|
|
1091
|
+
b = b.larg;
|
|
1092
|
+
const star = this.starOf(b);
|
|
1093
|
+
if (star !== null)
|
|
1094
|
+
return this.outputFields(star);
|
|
1095
|
+
const targets = (b.targetList ?? []);
|
|
1096
|
+
const out = targets.map((t) => { const x = this.typed(bodyOf(t).val); return x === null ? null : { oid: x.type, typmod: x.typmod }; });
|
|
1097
|
+
return out.some((x) => x === null) ? null : out;
|
|
1098
|
+
}
|
|
1099
|
+
/** whether an expression holds an aggregate (outside a subquery) */
|
|
1100
|
+
hasAggregate(v) {
|
|
1101
|
+
if (Array.isArray(v))
|
|
1102
|
+
return v.some((x) => this.hasAggregate(x));
|
|
1103
|
+
if (v === null || typeof v !== "object")
|
|
1104
|
+
return false;
|
|
1105
|
+
if (isNode(v)) {
|
|
1106
|
+
if (kindOf(v) === "SubLink")
|
|
1107
|
+
return false;
|
|
1108
|
+
if (kindOf(v) === "FuncCall" && bodyOf(v).over === undefined && this.typed(v)?.node === "AGGREF")
|
|
1109
|
+
return true;
|
|
1110
|
+
return Object.values(bodyOf(v)).some((x) => this.hasAggregate(x));
|
|
1111
|
+
}
|
|
1112
|
+
return Object.values(v).some((x) => this.hasAggregate(x));
|
|
1113
|
+
}
|
|
1114
|
+
/** whether a cell can be selected on its own in a grouped SELECT: a GROUP BY expression, an aggregate or a window
|
|
1115
|
+
* function, a constant, or built of those */
|
|
1116
|
+
validGrouped(n, groupShapes) {
|
|
1117
|
+
if (groupShapes.has(shapeOf(n)))
|
|
1118
|
+
return true;
|
|
1119
|
+
const k = kindOf(n);
|
|
1120
|
+
if (k === "A_Const")
|
|
1121
|
+
return true;
|
|
1122
|
+
// a column of this query Postgres accepted outside the GROUP BY is one the group's key decides: valid, and read
|
|
1123
|
+
// through any_value when the walk reaches its cell (decidedByKey)
|
|
1124
|
+
if (k === "ColumnRef") {
|
|
1125
|
+
const t = this.typed(n);
|
|
1126
|
+
return t?.node === "VAR" && t.levelsUp === 0;
|
|
1127
|
+
}
|
|
1128
|
+
if (k === "SubLink")
|
|
1129
|
+
return false;
|
|
1130
|
+
if (k === "FuncCall" && (bodyOf(n).over !== undefined || this.typed(n)?.node === "AGGREF"))
|
|
1131
|
+
return true;
|
|
1132
|
+
const children = Object.values(bodyOf(n)).flatMap((x) => Array.isArray(x) ? x : [x]).filter(isNode);
|
|
1133
|
+
return children.every((c) => this.validGrouped(c, groupShapes));
|
|
1134
|
+
}
|
|
1135
|
+
/** An expression as a program over cells (`cell` makes one), or null where it is not a numeric Postgres's rules
|
|
1136
|
+
* decide here; `kind` int where the value is an integer (integer arithmetic is DuckDB's, one cell). */
|
|
1137
|
+
exactOf(n, cell) {
|
|
1138
|
+
const k = kindOf(n), b = bodyOf(n);
|
|
1139
|
+
const type = this.typeOf(n);
|
|
1140
|
+
const asCell = () => type !== null && INTEGERS.has(type) ? { p: cell(n), kind: "int" } : type === NUMERIC ? { p: this.padded(n, cell(n)), kind: "numeric" } : null;
|
|
1141
|
+
const numeric = (p) => ({ p, kind: "numeric" });
|
|
1142
|
+
const sub = (x) => isNode(x) ? this.exactOf(x, cell) : null;
|
|
1143
|
+
switch (k) {
|
|
1144
|
+
// a numeric parameter is its bound text, as Postgres reads it (DuckDB is given it as a double: 15 digits)
|
|
1145
|
+
case "ParamRef": return type === NUMERIC ? numeric({ op: "cell", key: `$${b.number}` }) : asCell();
|
|
1146
|
+
case "A_Const": {
|
|
1147
|
+
if (b.ival !== undefined)
|
|
1148
|
+
return { p: { op: "literal", text: String(b.ival.ival ?? 0) }, kind: "int" };
|
|
1149
|
+
if (b.fval !== undefined)
|
|
1150
|
+
return numeric({ op: "literal", text: positional(String(b.fval.fval)) });
|
|
1151
|
+
const v = constString(n);
|
|
1152
|
+
const literal = v !== null && type === NUMERIC ? numericLiteral(v) : null;
|
|
1153
|
+
if (literal !== null)
|
|
1154
|
+
return numeric({ op: "literal", text: literal });
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
case "A_Expr": {
|
|
1158
|
+
const name = lastName(b.name);
|
|
1159
|
+
if (b.kind !== "AEXPR_OP" || !["+", "-", "*", "/", "%"].includes(name))
|
|
1160
|
+
return asCell();
|
|
1161
|
+
if (type !== NUMERIC)
|
|
1162
|
+
return asCell();
|
|
1163
|
+
if (b.lexpr === undefined) {
|
|
1164
|
+
const r = sub(b.rexpr);
|
|
1165
|
+
return r === null ? null : name === "-" ? numeric({ op: "neg", arg: r.p }) : r;
|
|
1166
|
+
}
|
|
1167
|
+
const l = sub(b.lexpr), r = sub(b.rexpr);
|
|
1168
|
+
if (l === null || r === null)
|
|
1169
|
+
return null;
|
|
1170
|
+
const ops = { "+": "add", "-": "sub", "*": "mul", "/": "div", "%": "mod" };
|
|
1171
|
+
return numeric({ op: ops[name], args: [l.p, r.p] });
|
|
1172
|
+
}
|
|
1173
|
+
case "TypeCast": {
|
|
1174
|
+
const target = lastName(b.typeName?.names);
|
|
1175
|
+
if (target !== "numeric" && target !== "decimal")
|
|
1176
|
+
return asCell();
|
|
1177
|
+
const arg = b.arg;
|
|
1178
|
+
const from = this.typeOf(arg);
|
|
1179
|
+
const inner = from !== null && FLOATS.has(from) ? numeric(cell(arg, from === FLOAT4 ? "float4" : "float8"))
|
|
1180
|
+
// a text: the digits Postgres's numeric_in reads (`1.50` keeps its scale, `1_000.5` is 1000.5)
|
|
1181
|
+
: from !== null && TEXTS.has(from) && from !== UNKNOWN && constString(arg) === null ? numeric(cell(pgCall("text", "a1::numeric", [this.clone(arg)]), "text"))
|
|
1182
|
+
: sub(arg);
|
|
1183
|
+
if (inner === null)
|
|
1184
|
+
return null;
|
|
1185
|
+
const mods = (b.typeName?.typmods ?? []).map((m) => constInt(m));
|
|
1186
|
+
if (mods.length === 0)
|
|
1187
|
+
return numeric(inner.p);
|
|
1188
|
+
if (mods.some((m) => m === null))
|
|
1189
|
+
return null;
|
|
1190
|
+
return numeric({ op: "typmod", arg: inner.p, precision: mods[0], scale: mods[1] ?? 0 });
|
|
1191
|
+
}
|
|
1192
|
+
case "CaseExpr": {
|
|
1193
|
+
// each arm as its own program, and the arm the row took as an integer cell: the same CASE with each result its
|
|
1194
|
+
// arm's number (the ELSE the last; past the arms, a CASE without one, NULL). Only the taken arm is computed, as
|
|
1195
|
+
// in Postgres, and an arm's cells are read only on its rows (`CASE WHEN <its arm> THEN … END`): DuckDB computes
|
|
1196
|
+
// every cell on every row, and sqrt(x) of an arm `WHEN x > 0` raised on the rest
|
|
1197
|
+
if (type !== NUMERIC)
|
|
1198
|
+
return asCell();
|
|
1199
|
+
const whens = (b.args ?? []);
|
|
1200
|
+
const index = () => ({ CaseExpr: { ...(b.arg === undefined ? {} : { arg: this.clone(b.arg) }),
|
|
1201
|
+
args: whens.map((w, i) => ({ CaseWhen: { expr: this.clone(bodyOf(w).expr), result: intConst(i) } })), defresult: intConst(whens.length) } });
|
|
1202
|
+
const onItsRows = (i) => (m, conv) => cell(kindOf(m) === "ColumnRef" || !holdsKind(m, "ColumnRef") ? m : caseWhen([[op("=", index(), intConst(i)), m]], null), conv);
|
|
1203
|
+
const results = [...whens.map((w) => bodyOf(w).result), ...(b.defresult === undefined ? [] : [b.defresult])];
|
|
1204
|
+
const arms = results.map((r, i) => isNode(r) ? this.exactOf(r, onItsRows(i)) : null);
|
|
1205
|
+
if (arms.some((a) => a === null))
|
|
1206
|
+
return asCell();
|
|
1207
|
+
return numeric({ op: "pick", index: cell(index()), args: arms.map((a) => a.p) });
|
|
1208
|
+
}
|
|
1209
|
+
case "MinMaxExpr":
|
|
1210
|
+
case "CoalesceExpr": {
|
|
1211
|
+
if (type !== NUMERIC)
|
|
1212
|
+
return asCell();
|
|
1213
|
+
const args = (b.args ?? []).map(sub);
|
|
1214
|
+
if (args.some((a) => a === null))
|
|
1215
|
+
return null;
|
|
1216
|
+
const opName = k === "CoalesceExpr" ? "coalesce" : b.op === "IS_GREATEST" ? "greatest" : "least";
|
|
1217
|
+
return numeric({ op: opName, args: args.map((a) => a.p) });
|
|
1218
|
+
}
|
|
1219
|
+
case "FuncCall": {
|
|
1220
|
+
const name = lastName(b.funcname);
|
|
1221
|
+
const args = (b.args ?? []);
|
|
1222
|
+
const plain = b.over === undefined && b.agg_within_group !== true;
|
|
1223
|
+
if (type !== NUMERIC)
|
|
1224
|
+
return asCell();
|
|
1225
|
+
switch (name) {
|
|
1226
|
+
case "round":
|
|
1227
|
+
case "trunc": {
|
|
1228
|
+
if (!plain || args.length < 1 || args.length > 2)
|
|
1229
|
+
return asCell();
|
|
1230
|
+
const x = sub(args[0]);
|
|
1231
|
+
const scale = args.length === 2 ? constInt(args[1]) : 0;
|
|
1232
|
+
if (x === null || scale === null)
|
|
1233
|
+
return asCell();
|
|
1234
|
+
return numeric({ op: name, arg: x.p, scale });
|
|
1235
|
+
}
|
|
1236
|
+
case "abs":
|
|
1237
|
+
case "floor":
|
|
1238
|
+
case "ceil":
|
|
1239
|
+
case "ceiling": {
|
|
1240
|
+
const x = args.length === 1 ? sub(args[0]) : null;
|
|
1241
|
+
return x === null ? asCell() : numeric({ op: name === "ceiling" ? "ceil" : name, arg: x.p });
|
|
1242
|
+
}
|
|
1243
|
+
case "mod": {
|
|
1244
|
+
const x = sub(args[0]), y = sub(args[1]);
|
|
1245
|
+
return x === null || y === null ? asCell() : numeric({ op: "mod", args: [x.p, y.p] });
|
|
1246
|
+
}
|
|
1247
|
+
case "ln":
|
|
1248
|
+
case "log":
|
|
1249
|
+
case "log10":
|
|
1250
|
+
case "exp":
|
|
1251
|
+
case "sqrt":
|
|
1252
|
+
case "power":
|
|
1253
|
+
case "pow": {
|
|
1254
|
+
const text = this.pgNumeric(n);
|
|
1255
|
+
return text === null ? asCell() : numeric(cell(text, "text"));
|
|
1256
|
+
}
|
|
1257
|
+
case "avg": {
|
|
1258
|
+
// Postgres's avg of an integer or a numeric is its sum divided by its count, both exact (int8_avg,
|
|
1259
|
+
// numeric_avg): DuckDB computes each, with the same DISTINCT, FILTER and window
|
|
1260
|
+
if (b.agg_within_group === true || args.length !== 1 || b.agg_star === true)
|
|
1261
|
+
return asCell();
|
|
1262
|
+
const argType = this.typeOf(args[0]);
|
|
1263
|
+
if (argType === null || !(INTEGERS.has(argType) || argType === NUMERIC))
|
|
1264
|
+
return asCell();
|
|
1265
|
+
const as = (f) => ({ FuncCall: { ...this.clone(b), funcname: [S(f)] } });
|
|
1266
|
+
const sum = as("sum");
|
|
1267
|
+
return numeric({ op: "div", args: [this.summed(b, args[0], sum, cell), cell(as("count"))] });
|
|
1268
|
+
}
|
|
1269
|
+
case "stddev":
|
|
1270
|
+
case "stddev_samp":
|
|
1271
|
+
case "stddev_pop":
|
|
1272
|
+
case "variance":
|
|
1273
|
+
case "var_samp":
|
|
1274
|
+
case "var_pop": {
|
|
1275
|
+
// Postgres's stddev and variance of an integer or a numeric are exact (numeric_stddev_internal), from N, Σx
|
|
1276
|
+
// and Σx², which DuckDB computes exactly, with the same FILTER and window. Not DISTINCT: the distinct values'
|
|
1277
|
+
// squares are not the distinct squares (-1 and 1)
|
|
1278
|
+
if (b.agg_within_group === true || args.length !== 1 || b.agg_star === true || b.agg_distinct === true)
|
|
1279
|
+
return asCell();
|
|
1280
|
+
const argType = this.typeOf(args[0]);
|
|
1281
|
+
if (argType === null || !(INTEGERS.has(argType) || argType === NUMERIC))
|
|
1282
|
+
return asCell();
|
|
1283
|
+
// a numeric only where its type bounds its digits: numeric(p ≤ 18, s), read as its unscaled bigint (x·10^s).
|
|
1284
|
+
// An unconstrained one (copied at the widest scale its values use) or a wider one could overflow DuckDB's 38
|
|
1285
|
+
// digits, where Postgres answers: DuckDB's double, the numeric-stddev gap
|
|
1286
|
+
const typmod = this.typed(n)?.args[0]?.typmod ?? -1;
|
|
1287
|
+
const precision = typmod >= 4 ? ((typmod - 4) >> 16) & 0xffff : null, scale = typmod >= 4 ? (typmod - 4) & 0xffff : 0;
|
|
1288
|
+
if (argType === NUMERIC && (precision === null || precision > 18))
|
|
1289
|
+
return asCell();
|
|
1290
|
+
const as = (f, arg) => ({ FuncCall: { ...this.clone(b), funcname: [S(f)], ...(arg === undefined ? {} : { args: [arg] }) } });
|
|
1291
|
+
const x = args[0];
|
|
1292
|
+
const sum = as("sum");
|
|
1293
|
+
// x² of an int2 or int4 as a HUGEINT
|
|
1294
|
+
const squares = () => cell(as("sum", op("*", cast(this.clone(x), ["hugeint"]), cast(this.clone(x), ["hugeint"]))));
|
|
1295
|
+
// a bigint's squares may pass a HUGEINT's 1.7e38 (two near 2^63): u = h·2^32 + l, so Σu² = Σh²·2^64 +
|
|
1296
|
+
// Σhl·2^33 + Σl², three sums that fit; a numeric(p ≤ 18, s) is u·10^-s, its Σx² that times 10^-2s
|
|
1297
|
+
const splitSquares = (u, s) => {
|
|
1298
|
+
const h = () => cast(op(">>", u(), intConst(32)), ["hugeint"]);
|
|
1299
|
+
const l = () => cast(op("&", u(), numConst("4294967295")), ["hugeint"]);
|
|
1300
|
+
const term = (a, c, by) => ({ op: "mul", args: [cell(as("sum", op("*", a, c))), { op: "literal", text: String(by) }] });
|
|
1301
|
+
const whole = { op: "add", args: [term(h(), h(), 2n ** 64n), { op: "add", args: [term(h(), l(), 2n ** 33n), term(l(), l(), 1n)] }] };
|
|
1302
|
+
return s === 0 ? whole : { op: "mul", args: [whole, { op: "literal", text: `0.${"0".repeat(2 * s - 1)}1` }] };
|
|
1303
|
+
};
|
|
1304
|
+
// the unscaled value: exact, as the copy holds a numeric(p, s) with at most s decimals
|
|
1305
|
+
const unscaled = () => cast(op("*", this.clone(x), cast(scale <= 9 ? intConst(10 ** scale) : numConst(String(10n ** BigInt(scale))), ["decimal", "38", "0"])), ["bigint"]);
|
|
1306
|
+
const sumX2 = argType === NUMERIC ? splitSquares(unscaled, scale) : argType === INT8 ? splitSquares(() => this.clone(x), 0) : squares();
|
|
1307
|
+
return numeric({ op: "stddev", args: [cell(as("count")), this.padded(sum, cell(sum)), sumX2],
|
|
1308
|
+
sample: !name.endsWith("_pop"), variance: name.startsWith("var") });
|
|
1309
|
+
}
|
|
1310
|
+
case "sum": {
|
|
1311
|
+
// Postgres's sum of a numeric has the most decimals a value SUMMED has (add_var keeps the larger dscale): of
|
|
1312
|
+
// a CASE, the arms the rows took, where DuckDB's sum has its CASE type's — the widest arm's, taken or not
|
|
1313
|
+
// (Metabase's "sum where", `sum(CASE WHEN … THEN amount ELSE 0.0 END)`, read 5.0 for 5)
|
|
1314
|
+
if (args.length !== 1 || b.agg_star === true || b.agg_within_group === true || this.typeOf(args[0]) !== NUMERIC)
|
|
1315
|
+
return asCell();
|
|
1316
|
+
return numeric(this.summed(b, args[0], n, cell));
|
|
1317
|
+
}
|
|
1318
|
+
case "min":
|
|
1319
|
+
case "max": {
|
|
1320
|
+
// Postgres's min / max of a numeric is one row's value, with that row's scale (numeric_smaller). Of an
|
|
1321
|
+
// expression computed here (a quotient), DuckDB picks the row — by its value as DuckDB has it (a quotient's
|
|
1322
|
+
// double orders as Postgres's numerics, __mason_numeric_div), its operands breaking a tie, so every operand
|
|
1323
|
+
// is the same row's — and the program computes that row's operands. A NULL operand is kept (_null): a
|
|
1324
|
+
// coalesce may read it. Of equal values whose scales differ, Postgres keeps the last it reads (scan order),
|
|
1325
|
+
// Mason the one whose operands are least (greatest for max): docs/SQL-SUPPORT.md `min-max-scale-tie`
|
|
1326
|
+
if (!plain || args.length !== 1 || b.agg_star === true || this.typeOf(args[0]) !== NUMERIC)
|
|
1327
|
+
return asCell();
|
|
1328
|
+
const operands = [];
|
|
1329
|
+
const probe = this.exactOf(args[0], (m) => { operands.push(m); return { op: "cell", key: String(operands.length - 1) }; });
|
|
1330
|
+
// a column's own min is DuckDB's exact decimal, padded to its scale
|
|
1331
|
+
if (probe === null || probe.p.op === "cell" || (probe.p.op === "pad" && probe.p.arg.op === "cell") || !worthComputing(probe.p))
|
|
1332
|
+
return asCell();
|
|
1333
|
+
const key = caseWhen([[isNotNull(this.clone(args[0])), row([this.clone(args[0]), ...operands.map((m) => this.clone(m))])]], null);
|
|
1334
|
+
const picked = (m) => {
|
|
1335
|
+
const call = fn(name === "min" ? "arg_min_null" : "arg_max_null", [this.clone(m), this.clone(key)], b.agg_filter === undefined ? {} : { agg_filter: this.clone(b.agg_filter) });
|
|
1336
|
+
// an aggregate, as the grouping checks read it (validGrouped, decidedByKey), typed as the operand it picks
|
|
1337
|
+
const t = this.typed(m);
|
|
1338
|
+
this.synthetic.set(bodyOf(call), { node: "AGGREF", type: t?.type ?? NUMERIC, typmod: t?.typmod ?? -1, args: [], oid: null, format: null,
|
|
1339
|
+
collation: t?.collation ?? 0, inputCollation: 0, levelsUp: null, datum: null, qualifier: null, renamed: null });
|
|
1340
|
+
return call;
|
|
1341
|
+
};
|
|
1342
|
+
const built = this.exactOf(args[0], (m, conv) => cell(picked(m), conv));
|
|
1343
|
+
return built === null ? asCell() : numeric(built.p);
|
|
1344
|
+
}
|
|
1345
|
+
default: return asCell();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
default: return asCell();
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
/** a numeric cell at Postgres's display scale where its types decide one (`displayScale`): DuckDB may hold the value
|
|
1352
|
+
* with fewer decimals (a numeric(65,30) is copied at the decimals its values use, src/duck/sync.ts) */
|
|
1353
|
+
padded(n, p) {
|
|
1354
|
+
const scale = this.displayScale(n);
|
|
1355
|
+
return scale !== null && scale > 0 ? { op: "pad", arg: p, scale } : p;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* A numeric sum (`sum`, DuckDB's sum of `x` with the aggregate `agg`'s DISTINCT, FILTER and window) at Postgres's
|
|
1359
|
+
* scale: the types' where they decide it (`padded`); where the rows do (`rowScale`: a CASE's arms, a coalesce), the
|
|
1360
|
+
* most decimals a value summed has, DuckDB's max of each row's scale beside its sum, under the same FILTER and
|
|
1361
|
+
* window. Not of DISTINCT values: of 5 and 5.0 Postgres keeps one, and which is its sort's
|
|
1362
|
+
* (docs/SQL-SUPPORT.md `mixed-scale-numeric`)
|
|
1363
|
+
*/
|
|
1364
|
+
summed(agg, x, sum, cell) {
|
|
1365
|
+
if (this.displayScale(x) !== null || agg.agg_distinct === true)
|
|
1366
|
+
return this.padded(sum, cell(sum));
|
|
1367
|
+
// a text read as an unconstrained numeric (Metabase's every JSON number), an ln or a sqrt: DuckDB holds it as a
|
|
1368
|
+
// double, so it is summed as Postgres computes it (`pgNumeric`), in two exact DECIMAL(38,18) parts (a value they cannot hold refused, `numeric-exact-sum`), at the
|
|
1369
|
+
// most decimals a row's text has. A sum holding a NaN or an infinity is the double's (NaN, ±Infinity, as Postgres adds
|
|
1370
|
+
// them)
|
|
1371
|
+
const text = this.pgNumeric(x);
|
|
1372
|
+
if (text !== null) {
|
|
1373
|
+
const read = () => this.clone(text);
|
|
1374
|
+
const aggregate = (name, arg, type) => {
|
|
1375
|
+
const n = { FuncCall: { ...this.clone(agg), funcname: [S(name)], args: [arg] } };
|
|
1376
|
+
this.synthetic.set(bodyOf(n), { node: "AGGREF", type, typmod: -1, args: [], oid: null, format: null, collation: 0, inputCollation: 0,
|
|
1377
|
+
levelsUp: null, datum: null, qualifier: null, renamed: null });
|
|
1378
|
+
return n;
|
|
1379
|
+
};
|
|
1380
|
+
const part = (i) => cell(aggregate("sum", cast(fn("__mason_numeric_exact", [read(), intConst(i)]), ["decimal", "38", "18"]), NUMERIC));
|
|
1381
|
+
const exact = { op: "add", args: [part(0), { op: "mul", args: [part(1), { op: "literal", text: `0.${"0".repeat(17)}1` }] }] };
|
|
1382
|
+
const scales = aggregate("max", fn("__mason_numeric_scale", [read()]), INT4);
|
|
1383
|
+
const special = { CoalesceExpr: { args: [aggregate("max", cast(fn("__mason_numeric_special", [read()]), ["integer"]), INT4), intConst(0)] } };
|
|
1384
|
+
return { op: "pick", index: cell(special), args: [{ op: "rescale", arg: exact, scale: cell(scales) }, cell(sum, "float8")] };
|
|
1385
|
+
}
|
|
1386
|
+
const scale = this.rowScale(x);
|
|
1387
|
+
if (scale === null)
|
|
1388
|
+
return this.padded(sum, cell(sum));
|
|
1389
|
+
const most = { FuncCall: { ...this.clone(agg), funcname: [S("max")], args: [caseWhen([[isNotNull(this.clone(x)), scale]], null)] } };
|
|
1390
|
+
// an aggregate, as the grouping checks read it (validGrouped, decidedByKey)
|
|
1391
|
+
this.synthetic.set(bodyOf(most), { node: "AGGREF", type: INT4, typmod: -1, args: [], oid: null, format: null, collation: 0, inputCollation: 0,
|
|
1392
|
+
levelsUp: null, datum: null, qualifier: null, renamed: null });
|
|
1393
|
+
return { op: "rescale", arg: cell(sum), scale: cell(most) };
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* A numeric DuckDB answers as a double whose every row's value Postgres's own function computes (src/duck/pg-eval.ts),
|
|
1397
|
+
* as numeric_out writes it: a text read as an unconstrained numeric (`(j ->> 'k')::numeric`), and ln, log, exp, sqrt
|
|
1398
|
+
* and power, whose digits follow a scale from the argument's own (numeric.c's ln_var, power_var) — each argument at
|
|
1399
|
+
* the scale Postgres gives it: a numeric(p, s) or an expression its types decide at s (DuckDB may hold fewer decimals:
|
|
1400
|
+
* src/duck/sync.ts), an integer whole, a literal as written, a column of no declared scale at the decimals the copy
|
|
1401
|
+
* holds (`mixed-scale-numeric`). Null for anything else.
|
|
1402
|
+
*/
|
|
1403
|
+
pgNumeric(n) {
|
|
1404
|
+
const b = bodyOf(n);
|
|
1405
|
+
if (kindOf(n) === "TypeCast") {
|
|
1406
|
+
const target = lastName(b.typeName?.names);
|
|
1407
|
+
if ((target !== "numeric" && target !== "decimal") || (b.typeName?.typmods ?? []).length > 0 || !isNode(b.arg))
|
|
1408
|
+
return null;
|
|
1409
|
+
const from = this.typeOf(b.arg);
|
|
1410
|
+
return from !== null && TEXTS.has(from) && from !== UNKNOWN && constString(b.arg) === null ? pgCall("text", "a1::numeric", [this.clone(b.arg)]) : null;
|
|
1411
|
+
}
|
|
1412
|
+
const name = kindOf(n) === "FuncCall" ? lastName(b.funcname) : "";
|
|
1413
|
+
const args = (b.args ?? []);
|
|
1414
|
+
if (!["ln", "log", "log10", "exp", "sqrt", "power", "pow"].includes(name) || this.typeOf(n) !== NUMERIC || b.over !== undefined || args.length < 1 || args.length > 2)
|
|
1415
|
+
return null;
|
|
1416
|
+
// an argument computed here (a quotient): its exact value, the program DuckDB evaluates row by row
|
|
1417
|
+
const read = args.map((a) => {
|
|
1418
|
+
const own = this.typed(a);
|
|
1419
|
+
if (own !== null && INTEGERS.has(own.type))
|
|
1420
|
+
return ["numeric", this.clone(a)];
|
|
1421
|
+
const scale = this.displayScale(a);
|
|
1422
|
+
if (scale !== null)
|
|
1423
|
+
return [`numeric(1000, ${scale})`, this.clone(a)];
|
|
1424
|
+
if (kindOf(a) === "ColumnRef" && own?.type === NUMERIC)
|
|
1425
|
+
return ["numeric", this.clone(a)];
|
|
1426
|
+
const exact = this.exactText(a);
|
|
1427
|
+
return exact === null ? null : ["numeric", exact];
|
|
1428
|
+
});
|
|
1429
|
+
if (read.some((x) => x === null)) {
|
|
1430
|
+
throw new Unrewritable(`${name}() of a numeric Mason cannot compute exactly is not supported by Mason`, `cast the argument to numeric(p, s): ${name}(x::numeric(20, 4))`, "numeric-math");
|
|
1431
|
+
}
|
|
1432
|
+
return pgCall("text", `${name}(${read.map((x, i) => `a${i + 1}::${x[0]}`).join(", ")})`, read.map((x) => x[1]));
|
|
1433
|
+
}
|
|
1434
|
+
/** whether DuckDB holds an expression's value exactly (an integer, or a DECIMAL its types decide): a column, an
|
|
1435
|
+
* expression of declared scales, a sum / min / max of one, a count; a CASE the rewrite made of such arms (an exact
|
|
1436
|
+
* program's arm index, a cell on its arm's rows). Not what DuckDB computes as a double (`^`, a quotient). */
|
|
1437
|
+
duckExact(n) {
|
|
1438
|
+
const t = this.typed(n), b = bodyOf(n);
|
|
1439
|
+
if (t !== null && INTEGERS.has(t.type))
|
|
1440
|
+
return true;
|
|
1441
|
+
if (kindOf(n) === "ColumnRef")
|
|
1442
|
+
return t !== null && t.type === NUMERIC;
|
|
1443
|
+
if (this.displayScale(n) !== null)
|
|
1444
|
+
return true;
|
|
1445
|
+
const args = (b.args ?? []);
|
|
1446
|
+
// a count (an avg's, made from the avg's node, carries the avg's type)
|
|
1447
|
+
if (kindOf(n) === "FuncCall" && lastName(b.funcname) === "count")
|
|
1448
|
+
return true;
|
|
1449
|
+
if (kindOf(n) === "FuncCall" && ["sum", "min", "max", "any_value"].includes(lastName(b.funcname)) && args.length === 1)
|
|
1450
|
+
return this.duckExact(args[0]);
|
|
1451
|
+
if (kindOf(n) === "CaseExpr" && t === null)
|
|
1452
|
+
return [...args.map((w) => bodyOf(w).result), ...(b.defresult === undefined ? [] : [b.defresult])].every((r) => isNode(r) && this.duckExact(r));
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
/** a numeric's exact text, row by row: its program (exactOf) evaluated by DuckDB over its cells' texts
|
|
1456
|
+
* (`__mason_numeric_program`, src/duck/functions.ts); null where there is none, or it reads a parameter */
|
|
1457
|
+
exactText(n) {
|
|
1458
|
+
const cells = [];
|
|
1459
|
+
let built;
|
|
1460
|
+
try {
|
|
1461
|
+
built = this.exactOf(n, (m, conv = "exact") => { cells.push({ node: m, conv }); return { op: "cell", key: String(cells.length - 1) }; });
|
|
1462
|
+
}
|
|
1463
|
+
catch (e) {
|
|
1464
|
+
if (e === NOT_EXACT)
|
|
1465
|
+
return null;
|
|
1466
|
+
throw e;
|
|
1467
|
+
}
|
|
1468
|
+
if (built === null || cellsOfProgram(built.p).some((k) => k.startsWith("$")))
|
|
1469
|
+
return null;
|
|
1470
|
+
if (cells.some((c) => c.conv === "exact" && !this.duckExact(c.node)))
|
|
1471
|
+
return null;
|
|
1472
|
+
const texts = cells.map((c) => c.conv === "text" ? this.clone(c.node) : c.conv === "exact" ? cast(this.clone(c.node), ["varchar"])
|
|
1473
|
+
: fn("__mason_float_numeric_text", [cast(this.clone(c.node), ["double"]), boolConst(c.conv === "float4")]));
|
|
1474
|
+
return fn("__mason_numeric_program", [strConst(JSON.stringify(built.p)), ...texts]);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* The decimals Postgres writes for one ROW's value of a numeric expression, as DuckDB computes them (an integer), where
|
|
1478
|
+
* the arm a row took decides them: a CASE's arms, a coalesce's first value that is not NULL, + - * over those
|
|
1479
|
+
* (numeric.c's add_var, mul_var). Null where a value's own digits decide (an unconstrained column, a division).
|
|
1480
|
+
*/
|
|
1481
|
+
rowScale(n) {
|
|
1482
|
+
const fixed = this.displayScale(n);
|
|
1483
|
+
if (fixed !== null)
|
|
1484
|
+
return intConst(fixed);
|
|
1485
|
+
const k = kindOf(n), b = bodyOf(n);
|
|
1486
|
+
const of = (x) => isNode(x) ? this.rowScale(x) : null;
|
|
1487
|
+
switch (k) {
|
|
1488
|
+
case "CaseExpr": {
|
|
1489
|
+
const whens = (b.args ?? []);
|
|
1490
|
+
const results = whens.map((w) => of(bodyOf(w).result));
|
|
1491
|
+
const otherwise = b.defresult === undefined ? nullConst() : of(b.defresult);
|
|
1492
|
+
if (results.some((r) => r === null) || otherwise === null)
|
|
1493
|
+
return null;
|
|
1494
|
+
return { CaseExpr: { ...(b.arg === undefined ? {} : { arg: this.clone(b.arg) }),
|
|
1495
|
+
args: whens.map((w, i) => ({ CaseWhen: { expr: this.clone(bodyOf(w).expr), result: results[i] } })), defresult: otherwise } };
|
|
1496
|
+
}
|
|
1497
|
+
case "CoalesceExpr": {
|
|
1498
|
+
const args = (b.args ?? []);
|
|
1499
|
+
const scales = args.map(of);
|
|
1500
|
+
if (scales.some((r) => r === null))
|
|
1501
|
+
return null;
|
|
1502
|
+
return caseWhen(args.map((a, i) => [isNotNull(this.clone(a)), scales[i]]), null);
|
|
1503
|
+
}
|
|
1504
|
+
case "A_Expr": {
|
|
1505
|
+
if (b.kind !== "AEXPR_OP")
|
|
1506
|
+
return null;
|
|
1507
|
+
const name = lastName(b.name);
|
|
1508
|
+
const r = of(b.rexpr);
|
|
1509
|
+
if (b.lexpr === undefined)
|
|
1510
|
+
return name === "-" || name === "+" ? r : null;
|
|
1511
|
+
const l = of(b.lexpr);
|
|
1512
|
+
if (l === null || r === null)
|
|
1513
|
+
return null;
|
|
1514
|
+
return name === "+" || name === "-" ? fn("greatest", [l, r]) : name === "*" ? op("+", l, r) : null;
|
|
1515
|
+
}
|
|
1516
|
+
case "TypeCast": {
|
|
1517
|
+
const target = lastName(b.typeName?.names);
|
|
1518
|
+
return (target === "numeric" || target === "decimal") && (b.typeName?.typmods ?? []).length === 0 && isNode(b.arg) && this.typeOf(b.arg) === NUMERIC ? of(b.arg) : null;
|
|
1519
|
+
}
|
|
1520
|
+
default: return null;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* The decimals Postgres writes for an expression's every value, where its types alone decide them: a numeric(p, s)
|
|
1525
|
+
* column's s, a literal's own, a sum / min / max of one, + and - the larger, * the sum (numeric.c's add_var, mul_var).
|
|
1526
|
+
* Null where the values decide (an unconstrained column, a division): DuckDB's own scale stands there.
|
|
1527
|
+
*/
|
|
1528
|
+
displayScale(n) {
|
|
1529
|
+
const k = kindOf(n), b = bodyOf(n);
|
|
1530
|
+
const t = this.typed(n);
|
|
1531
|
+
if (t !== null && INTEGERS.has(t.type))
|
|
1532
|
+
return 0;
|
|
1533
|
+
switch (k) {
|
|
1534
|
+
case "ColumnRef": return t !== null && t.type === NUMERIC && t.typmod >= 4 ? (t.typmod - 4) & 0xffff : null;
|
|
1535
|
+
case "A_Const": return b.ival !== undefined ? 0 : b.fval !== undefined ? (positional(String(b.fval.fval)).split(".")[1] ?? "").length : null;
|
|
1536
|
+
case "A_Expr": {
|
|
1537
|
+
if (b.kind !== "AEXPR_OP")
|
|
1538
|
+
return null;
|
|
1539
|
+
const name = lastName(b.name);
|
|
1540
|
+
const r = isNode(b.rexpr) ? this.displayScale(b.rexpr) : null;
|
|
1541
|
+
if (b.lexpr === undefined)
|
|
1542
|
+
return name === "-" || name === "+" ? r : null;
|
|
1543
|
+
const l = isNode(b.lexpr) ? this.displayScale(b.lexpr) : null;
|
|
1544
|
+
if (l === null || r === null)
|
|
1545
|
+
return null;
|
|
1546
|
+
return name === "+" || name === "-" ? Math.max(l, r) : name === "*" ? l + r : null;
|
|
1547
|
+
}
|
|
1548
|
+
case "FuncCall": {
|
|
1549
|
+
const args = (b.args ?? []);
|
|
1550
|
+
if (!["sum", "min", "max"].includes(lastName(b.funcname)) || args.length !== 1 || b.agg_star === true || b.agg_within_group === true)
|
|
1551
|
+
return null;
|
|
1552
|
+
return this.displayScale(args[0]);
|
|
1553
|
+
}
|
|
1554
|
+
case "TypeCast": {
|
|
1555
|
+
const target = lastName(b.typeName?.names);
|
|
1556
|
+
if (target !== "numeric" && target !== "decimal")
|
|
1557
|
+
return null;
|
|
1558
|
+
const mods = (b.typeName?.typmods ?? []).map((m) => constInt(m));
|
|
1559
|
+
if (mods.length === 0) {
|
|
1560
|
+
if (!isNode(b.arg))
|
|
1561
|
+
return null;
|
|
1562
|
+
const from = this.typeOf(b.arg);
|
|
1563
|
+
// an integer is whole; a literal text has the decimals it is written with (`'1.50'::numeric`)
|
|
1564
|
+
if (from !== null && INTEGERS.has(from))
|
|
1565
|
+
return 0;
|
|
1566
|
+
const literal = constString(b.arg) === null ? null : numericLiteral(constString(b.arg));
|
|
1567
|
+
if (literal !== null)
|
|
1568
|
+
return (literal.split(".")[1] ?? "").length;
|
|
1569
|
+
return from === NUMERIC ? this.displayScale(b.arg) : null;
|
|
1570
|
+
}
|
|
1571
|
+
return mods.some((m) => m === null) ? null : mods[1] ?? 0;
|
|
1572
|
+
}
|
|
1573
|
+
default: return null;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
// ------------------------------------------------------------------------------------- sorting and collation
|
|
1577
|
+
/** the collation a text expression compares under, as DuckDB follows it; null for a type that is no text */
|
|
1578
|
+
textCollation(n) {
|
|
1579
|
+
if (kindOf(n) === "CollateClause")
|
|
1580
|
+
return this.namedCollation(lastName(bodyOf(n).collname));
|
|
1581
|
+
const t = this.typed(n);
|
|
1582
|
+
if (t === null || !TEXTS.has(t.type))
|
|
1583
|
+
return null;
|
|
1584
|
+
if (BYTE_COLLATIONS.has(t.collation))
|
|
1585
|
+
return { kind: "bytes" };
|
|
1586
|
+
return this.collation;
|
|
1587
|
+
}
|
|
1588
|
+
namedCollation(name) {
|
|
1589
|
+
if (["c", "posix", "ucs_basic", "pg_c_utf8"].includes(name))
|
|
1590
|
+
return { kind: "bytes" };
|
|
1591
|
+
if (name === "default")
|
|
1592
|
+
return this.collation;
|
|
1593
|
+
// Postgres's `unicode` is ICU's root locale, `und-x-icu` by another name
|
|
1594
|
+
if (name === "unicode")
|
|
1595
|
+
return { kind: "icu", name: "en_us" };
|
|
1596
|
+
const icu = /^(.*)-x-icu$/.exec(name);
|
|
1597
|
+
if (icu)
|
|
1598
|
+
return { kind: "icu", name: icu[1] === "und" ? "en_us" : icu[1].replace(/-/g, "_") };
|
|
1599
|
+
throw new Unrewritable(`collation "${name}" is not supported by Mason`, `write COLLATE "C" for byte order, or leave the COLLATE out for the database's`, "collation-unknown");
|
|
1600
|
+
}
|
|
1601
|
+
/** the keys DuckDB sorts by for one text expression under a collation */
|
|
1602
|
+
sortKeys(e, c) {
|
|
1603
|
+
const plain = kindOf(e) === "CollateClause" ? bodyOf(e).arg : e;
|
|
1604
|
+
switch (c.kind) {
|
|
1605
|
+
case "bytes": return [plain];
|
|
1606
|
+
case "icu": return [collate(plain, c.name)];
|
|
1607
|
+
case "libc": return libcKeys(this.cp, plain, c.icu);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
/** a SortBy: its expression's sort keys under its collation (several under a libc one), each with its direction */
|
|
1611
|
+
sortBy(b, targets, select) {
|
|
1612
|
+
let e = b.node;
|
|
1613
|
+
if (targets !== null) {
|
|
1614
|
+
const n = constInt(e);
|
|
1615
|
+
if (n !== null && n >= 1 && n <= targets.length)
|
|
1616
|
+
e = this.clone(bodyOf(targets[n - 1]).val);
|
|
1617
|
+
else if (kindOf(e) === "ColumnRef") {
|
|
1618
|
+
const f = names(bodyOf(e).fields);
|
|
1619
|
+
const named = f.length === 1 ? targets.filter((t) => bodyOf(t).name === f[0]) : [];
|
|
1620
|
+
if (named.length === 1)
|
|
1621
|
+
e = this.clone(bodyOf(named[0]).val);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
// a derived table's column read through `s.*` (`SELECT s.* FROM (…) AS s ORDER BY s.pod`, a tool's wrapper): Postgres
|
|
1625
|
+
// analysed the ORDER BY as the star's expansion, at no offset of its own, so the column has no type here — it is the
|
|
1626
|
+
// subquery's target of that name, whose type and collation decide the order (a review finding: sorted by bytes)
|
|
1627
|
+
if (kindOf(e) === "ColumnRef" && select !== null && this.typed(e) === null) {
|
|
1628
|
+
const target = this.derivedTarget(e, select);
|
|
1629
|
+
if (target !== null)
|
|
1630
|
+
this.typedLike.set(bodyOf(e), target);
|
|
1631
|
+
}
|
|
1632
|
+
const type = this.typeOf(e);
|
|
1633
|
+
if (type === INET)
|
|
1634
|
+
throw new Unrewritable("ORDER BY an inet value is not supported by Mason (Postgres orders addresses by network, then host)", "sort by another column", "inet-order");
|
|
1635
|
+
// a citext sorts by its lower-cased text under the database's collation (citext_cmp)
|
|
1636
|
+
if (this.isCitext(type)) {
|
|
1637
|
+
e = this.foldCitext(e);
|
|
1638
|
+
b.node = e;
|
|
1639
|
+
}
|
|
1640
|
+
this.comparable(e, "ORDER BY");
|
|
1641
|
+
const c = this.textCollation(e);
|
|
1642
|
+
// a char(n) sorts without its padding (bpcharcmp)
|
|
1643
|
+
if (this.typeOf(e) === BPCHAR) {
|
|
1644
|
+
e = fn("rtrim", [e]);
|
|
1645
|
+
this.typedLike.set(bodyOf(e), b.node);
|
|
1646
|
+
b.node = e;
|
|
1647
|
+
}
|
|
1648
|
+
if (c === null || (c.kind === "bytes" && kindOf(b.node) !== "CollateClause"))
|
|
1649
|
+
return null;
|
|
1650
|
+
return this.sortKeys(e, c).map((k) => ({ SortBy: { ...b, node: k } }));
|
|
1651
|
+
}
|
|
1652
|
+
/** the target a derived table's column names (`s.pod`, or `pod` where one derived table is read): the subquery's or
|
|
1653
|
+
* the CTE's target with that output name, when there is exactly one; null for a base table's column or an ambiguity */
|
|
1654
|
+
/** the FROM's subqueries and CTE references, by alias (a column list renames them: left out) */
|
|
1655
|
+
fromSources(select) {
|
|
1656
|
+
const sources = [];
|
|
1657
|
+
const visit = (x) => {
|
|
1658
|
+
if (kindOf(x) === "JoinExpr") {
|
|
1659
|
+
for (const side of [bodyOf(x).larg, bodyOf(x).rarg])
|
|
1660
|
+
if (isNode(side))
|
|
1661
|
+
visit(side);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
if (kindOf(x) === "RangeSubselect") {
|
|
1665
|
+
const a = bodyOf(x).alias?.aliasname;
|
|
1666
|
+
if (typeof a === "string" && isNode(bodyOf(x).subquery) && bodyOf(x).alias?.colnames === undefined)
|
|
1667
|
+
sources.push({ alias: a, query: bodyOf(x).subquery });
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
if (kindOf(x) === "RangeVar" && bodyOf(x).schemaname === undefined && bodyOf(x).alias?.colnames === undefined) {
|
|
1671
|
+
const cte = this.ctes.get(String(bodyOf(x).relname));
|
|
1672
|
+
if (cte !== undefined)
|
|
1673
|
+
sources.push({ alias: bodyOf(x).alias?.aliasname ?? String(bodyOf(x).relname), query: cte });
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
for (const x of (select.fromClause ?? []))
|
|
1677
|
+
if (isNode(x))
|
|
1678
|
+
visit(x);
|
|
1679
|
+
return sources;
|
|
1680
|
+
}
|
|
1681
|
+
derivedTarget(ref, select) {
|
|
1682
|
+
const f = names(bodyOf(ref).fields);
|
|
1683
|
+
if (f.length === 0 || f.length > 2)
|
|
1684
|
+
return null;
|
|
1685
|
+
const col = f[f.length - 1], alias = f.length === 2 ? f[0] : undefined;
|
|
1686
|
+
const sources = this.fromSources(select);
|
|
1687
|
+
const matching = alias === undefined ? sources : sources.filter((q) => q.alias === alias);
|
|
1688
|
+
if (matching.length !== 1)
|
|
1689
|
+
return null;
|
|
1690
|
+
let q = bodyOf(matching[0].query);
|
|
1691
|
+
while (q.op !== undefined && q.op !== "SETOP_NONE" && q.larg !== undefined)
|
|
1692
|
+
q = q.larg;
|
|
1693
|
+
const hits = (q.targetList ?? []).filter((t) => isNode(bodyOf(t).val) && !hasStar(bodyOf(t).val) && (bodyOf(t).name ?? figureColname(bodyOf(t).val)) === col);
|
|
1694
|
+
return hits.length === 1 ? bodyOf(hits[0]).val : null;
|
|
1695
|
+
}
|
|
1696
|
+
/** a GROUP BY item as the expression it groups: an ordinal or an output name is that target's */
|
|
1697
|
+
groupedExpression(g, b) {
|
|
1698
|
+
const targets = (b.targetList ?? []);
|
|
1699
|
+
const n = constInt(g);
|
|
1700
|
+
if (n !== null && n >= 1 && n <= targets.length)
|
|
1701
|
+
return bodyOf(targets[n - 1]).val;
|
|
1702
|
+
if (kindOf(g) === "ColumnRef") {
|
|
1703
|
+
const f = names(bodyOf(g).fields);
|
|
1704
|
+
const named = f.length === 1 && this.typed(g) === null ? targets.filter((t) => bodyOf(t).name === f[0]) : [];
|
|
1705
|
+
if (named.length === 1)
|
|
1706
|
+
return bodyOf(named[0]).val;
|
|
1707
|
+
}
|
|
1708
|
+
return g;
|
|
1709
|
+
}
|
|
1710
|
+
/** a value Mason cannot sort, group or compare as Postgres does: jsonb (Postgres's own order and equality), a text
|
|
1711
|
+
* array under the database's collation (each element under it) — refused, where a statement would need it */
|
|
1712
|
+
comparable(n, where) {
|
|
1713
|
+
const type = this.typeOf(n);
|
|
1714
|
+
if (this.isCitext(type))
|
|
1715
|
+
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
|
+
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)`, "compare a member (j ->> 'k'), or j::text", "jsonb-compare");
|
|
1718
|
+
if ((type === 1009 || type === 1015) && this.collation.kind !== "bytes")
|
|
1719
|
+
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
|
+
}
|
|
1721
|
+
collateClause(b) {
|
|
1722
|
+
// an expression's own COLLATE outside a sort: a comparison's is handled at the comparison; C needs nothing
|
|
1723
|
+
const c = this.namedCollation(lastName(b.collname));
|
|
1724
|
+
if (c.kind === "bytes")
|
|
1725
|
+
return b.arg;
|
|
1726
|
+
if (c.kind === "icu") {
|
|
1727
|
+
b.collname = [S(c.name)];
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
/** a text comparison under the collation: `a < b` compares their keys */
|
|
1733
|
+
compareText(name, l, r, c) {
|
|
1734
|
+
if (c.kind === "icu")
|
|
1735
|
+
return op(name, collate(strip(l), c.name), collate(strip(r), c.name));
|
|
1736
|
+
if (c.kind === "libc")
|
|
1737
|
+
return op(name, libcKey(this.cp, strip(l), c.icu), libcKey(this.cp, strip(r), c.icu));
|
|
1738
|
+
return op(name, l, r);
|
|
1739
|
+
}
|
|
1740
|
+
minMax(b) {
|
|
1741
|
+
// GREATEST / LEAST of text compare under the collation
|
|
1742
|
+
const t = this.at(b, ["MINMAXEXPR"]);
|
|
1743
|
+
if (t !== null)
|
|
1744
|
+
b.args = b.args.map((a, i) => this.widen(a, t.args[i]?.type));
|
|
1745
|
+
if (t === null || !TEXTS.has(t.type) || BYTE_COLLATIONS.has(t.inputCollation) || this.collation.kind === "bytes")
|
|
1746
|
+
return null;
|
|
1747
|
+
if (this.collation.kind === "libc") {
|
|
1748
|
+
// the greatest (least) of the values' keys, which end in the value's own bytes: the value is read back from them
|
|
1749
|
+
const icu = this.collation.icu;
|
|
1750
|
+
const keys = b.args.map((a) => libcKey(this.cp, strip(a), icu));
|
|
1751
|
+
const best = { MinMaxExpr: { ...b, args: keys } };
|
|
1752
|
+
this.done.add(bodyOf(best));
|
|
1753
|
+
return fn("decode", [fn("unhex", [fn("regexp_extract", [best, strConst("[^ ]*$")])])]);
|
|
1754
|
+
}
|
|
1755
|
+
const name = this.collation.name;
|
|
1756
|
+
b.args = b.args.map((a) => collate(a, name));
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
// --------------------------------------------------------------------------------------------- operators
|
|
1760
|
+
aExpr(b) {
|
|
1761
|
+
const name = lastName(b.name);
|
|
1762
|
+
const t = this.typed({ A_Expr: b });
|
|
1763
|
+
let l = b.lexpr, r = b.rexpr;
|
|
1764
|
+
const args = t?.args ?? [];
|
|
1765
|
+
const a0 = args[0]?.type ?? null, a1 = args[1]?.type ?? null;
|
|
1766
|
+
// jsonb's jsonpath operators: Postgres's answer for each row (src/duck/pg-eval.ts)
|
|
1767
|
+
if (b.kind === "AEXPR_OP" && (name === "@?" || name === "@@") && a0 === JSONB && l !== undefined && r !== undefined)
|
|
1768
|
+
return pgCall("bool", `a1::jsonb ${name} a2::jsonpath`, [l, r]);
|
|
1769
|
+
if ((b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL") && this.isCitext(a0)) {
|
|
1770
|
+
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
|
+
}
|
|
1772
|
+
switch (b.kind) {
|
|
1773
|
+
case "AEXPR_LIKE":
|
|
1774
|
+
case "AEXPR_ILIKE": {
|
|
1775
|
+
// DuckDB's LIKE has no escape character unless asked; Postgres's is the backslash
|
|
1776
|
+
const negated = name.startsWith("!");
|
|
1777
|
+
// a citext's LIKE is case-insensitive (texticlike)
|
|
1778
|
+
const ilike = b.kind === "AEXPR_ILIKE" || this.isCitext(a0);
|
|
1779
|
+
let pattern = r, escape = strConst("\\");
|
|
1780
|
+
if (kindOf(pattern) === "FuncCall" && lastName(bodyOf(pattern).funcname) === "like_escape") {
|
|
1781
|
+
const a = bodyOf(pattern).args;
|
|
1782
|
+
pattern = a[0];
|
|
1783
|
+
escape = a[1];
|
|
1784
|
+
}
|
|
1785
|
+
const call = fn(ilike ? "ilike_escape" : "like_escape", [l, pattern, escape]);
|
|
1786
|
+
return negated ? not(call) : call;
|
|
1787
|
+
}
|
|
1788
|
+
case "AEXPR_BETWEEN":
|
|
1789
|
+
case "AEXPR_NOT_BETWEEN": {
|
|
1790
|
+
const bounds = (bodyOf(r).items ?? []);
|
|
1791
|
+
// numbers of which one is a float are compared as double precision (float8 is Postgres's preferred numeric
|
|
1792
|
+
// type: `real BETWEEN 0.1 AND 1` is float48ge and float8le), where DuckDB's common type is single precision
|
|
1793
|
+
const own = [l, ...bounds].map((x) => this.typeOf(x));
|
|
1794
|
+
if (own.some((x) => x !== null && FLOATS.has(x)) && own.every((x) => x !== null && NUMBER_TYPES.has(x)) && new Set(own).size > 1) {
|
|
1795
|
+
b.lexpr = cast(l, ["double"]);
|
|
1796
|
+
bodyOf(r).items = bounds.map((x) => cast(x, ["double"]));
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
const c = this.textCollation(l);
|
|
1800
|
+
// char(n): compared without the padding
|
|
1801
|
+
const trim = (x) => this.typeOf(x) === BPCHAR ? fn("rtrim", [x]) : x;
|
|
1802
|
+
if (c === null || c.kind === "bytes") {
|
|
1803
|
+
if (own.includes(BPCHAR)) {
|
|
1804
|
+
b.lexpr = trim(l);
|
|
1805
|
+
bodyOf(r).items = bounds.map(trim);
|
|
1806
|
+
}
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
const inside = and([this.compareText(">=", trim(l), trim(bounds[0]), c), this.compareText("<=", trim(this.clone(l)), trim(bounds[1]), c)]);
|
|
1810
|
+
return b.kind === "AEXPR_NOT_BETWEEN" ? not(inside) : inside;
|
|
1811
|
+
}
|
|
1812
|
+
case "AEXPR_OP_ANY":
|
|
1813
|
+
case "AEXPR_OP_ALL": {
|
|
1814
|
+
// x LIKE ANY (patterns): each pattern's LIKE, OR'd (ALL: AND'd) — SQL's three values are ANY's and ALL's
|
|
1815
|
+
const like = { "~~": ["like_escape", false], "~~*": ["ilike_escape", false], "!~~": ["like_escape", true], "!~~*": ["ilike_escape", true] }[name];
|
|
1816
|
+
if (like === undefined) {
|
|
1817
|
+
// DuckDB reads a NULL array as an empty one (ANY false, ALL true), where Postgres's answer is NULL; with a NULL
|
|
1818
|
+
// element or a NULL operand it answers as Postgres does. An array written out is never NULL
|
|
1819
|
+
if (r === undefined || kindOf(r) === "A_ArrayExpr" || constString(r) !== null || this.nullArrayTested.has(b))
|
|
1820
|
+
return null;
|
|
1821
|
+
this.nullArrayTested.add(b);
|
|
1822
|
+
return caseWhen([[isNull(this.clone(r)), nullConst()]], { A_Expr: b });
|
|
1823
|
+
}
|
|
1824
|
+
const literal = constString(r);
|
|
1825
|
+
const patterns = r !== undefined && kindOf(r) === "A_ArrayExpr" ? (bodyOf(r).elements ?? [])
|
|
1826
|
+
: literal !== null ? arrayLiteral(literal).map((p) => p === null ? nullConst() : strConst(p)) : null;
|
|
1827
|
+
if (patterns === null)
|
|
1828
|
+
throw new Unrewritable(`${like[0] === "like_escape" ? "LIKE" : "ILIKE"} ${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} over an array that is not written out is not supported by Mason`, "write the patterns as ARRAY['…', '…'], or OR the LIKEs", "like-any-array");
|
|
1829
|
+
const each = patterns.map((p, i) => { const m = fn(like[0], [i === 0 ? l : this.clone(l), p, strConst("\\")]); return like[1] ? not(m) : m; });
|
|
1830
|
+
if (each.length === 0)
|
|
1831
|
+
return boolConst(b.kind === "AEXPR_OP_ALL");
|
|
1832
|
+
return b.kind === "AEXPR_OP_ANY" ? or(each) : and(each);
|
|
1833
|
+
}
|
|
1834
|
+
case "AEXPR_SIMILAR": {
|
|
1835
|
+
const call = bodyOf(r);
|
|
1836
|
+
const a = (call.args ?? []);
|
|
1837
|
+
const pattern = constString(a[0]), escape = a[1] === undefined ? "\\" : constString(a[1]);
|
|
1838
|
+
if (pattern === null || escape === null)
|
|
1839
|
+
throw new Unrewritable("SIMILAR TO with a pattern or an ESCAPE that is not a literal is not supported by Mason", "write the pattern as a string literal, or use ~ with a POSIX regular expression", "similar-to-forms");
|
|
1840
|
+
const m = fn("regexp_full_match", [l, strConst(`(?s)${similarToRe2(pattern, escape)}`)]);
|
|
1841
|
+
return name.startsWith("!") ? not(m) : m;
|
|
1842
|
+
}
|
|
1843
|
+
case "AEXPR_OP": break;
|
|
1844
|
+
case "AEXPR_IN": {
|
|
1845
|
+
// `x IN (a, b)` is x = ANY (ARRAY[a, b]) over the type Postgres chose for both
|
|
1846
|
+
const want = t?.args[0]?.type ?? null, element = t?.args[1]?.type !== undefined ? NUMBER_ELEMENT[t.args[1].type] ?? null : null;
|
|
1847
|
+
// a citext IN a list: each side lower-cased (citext_eq)
|
|
1848
|
+
if (this.isCitext(want) && l !== undefined && r !== undefined && kindOf(r) === "List") {
|
|
1849
|
+
b.lexpr = this.foldCitext(l);
|
|
1850
|
+
bodyOf(r).items = (bodyOf(r).items ?? []).map((x) => this.foldCitext(x));
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
if (l !== undefined)
|
|
1854
|
+
this.comparable(l, "IN");
|
|
1855
|
+
if (want === BPCHAR && l !== undefined && r !== undefined && kindOf(r) === "List") {
|
|
1856
|
+
b.lexpr = fn("rtrim", [l]);
|
|
1857
|
+
bodyOf(r).items = (bodyOf(r).items ?? []).map((x) => fn("rtrim", [x]));
|
|
1858
|
+
return null;
|
|
1859
|
+
}
|
|
1860
|
+
if (l !== undefined)
|
|
1861
|
+
b.lexpr = this.widen(l, want);
|
|
1862
|
+
if (r !== undefined && kindOf(r) === "List")
|
|
1863
|
+
bodyOf(r).items = (bodyOf(r).items ?? []).map((x) => this.widen(x, element));
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
default: return null;
|
|
1867
|
+
}
|
|
1868
|
+
const comparison = ["=", "<>", "!=", "<", "<=", ">", ">="].includes(name);
|
|
1869
|
+
// a citext beside a citext: compared lower-cased (citext_eq, citext_cmp); beside a text, Postgres's text operator
|
|
1870
|
+
// compares the text as it is, and so does this
|
|
1871
|
+
// (into l and r: what follows widens and pads THEM, and would put the column back)
|
|
1872
|
+
if (comparison && this.isCitext(a0) && this.isCitext(a1) && l !== undefined && r !== undefined) {
|
|
1873
|
+
l = b.lexpr = this.foldCitext(l);
|
|
1874
|
+
r = b.rexpr = this.foldCitext(r);
|
|
1875
|
+
}
|
|
1876
|
+
if (comparison && name !== "=" && name !== "<>" && name !== "!=" && (a0 === INET || a1 === INET)) {
|
|
1877
|
+
throw new Unrewritable(`comparing (${name}) inet values is not supported by Mason (Postgres orders addresses by network, then host)`, "compare them for equality, or compare host(x)::text", "inet-order");
|
|
1878
|
+
}
|
|
1879
|
+
// each operand as the type of the operator Postgres chose (`real / smallint` is float8's division, `real = 0.1`
|
|
1880
|
+
// float8's comparison): DuckDB would compute in its own common type (single precision, for those)
|
|
1881
|
+
if (l !== undefined)
|
|
1882
|
+
b.lexpr = this.widen(l, a0);
|
|
1883
|
+
if (r !== undefined)
|
|
1884
|
+
b.rexpr = this.widen(r, l === undefined ? a0 : a1);
|
|
1885
|
+
// char(n) beside char(n): compared without the padding (bpchareq, bpcharlt …)
|
|
1886
|
+
if (a0 === BPCHAR && a1 === BPCHAR && ["=", "<>", "!=", "<", "<=", ">", ">="].includes(name) && l !== undefined && r !== undefined) {
|
|
1887
|
+
b.lexpr = fn("rtrim", [l]);
|
|
1888
|
+
b.rexpr = fn("rtrim", [r]);
|
|
1889
|
+
this.typedLike.set(bodyOf(b.lexpr), l);
|
|
1890
|
+
this.typedLike.set(bodyOf(b.rexpr), r);
|
|
1891
|
+
}
|
|
1892
|
+
if (comparison && b.lexpr !== undefined)
|
|
1893
|
+
this.comparable(b.lexpr, `comparing (${name})`);
|
|
1894
|
+
switch (name) {
|
|
1895
|
+
case "/": {
|
|
1896
|
+
if (l === undefined || r === undefined)
|
|
1897
|
+
return null;
|
|
1898
|
+
if (a0 !== null && a1 !== null && INTEGERS.has(a0) && INTEGERS.has(a1))
|
|
1899
|
+
return op("//", b.lexpr, b.rexpr);
|
|
1900
|
+
// a numeric quotient is Postgres's, rounded at its scale (numeric_div), so equal quotients are one value where
|
|
1901
|
+
// DuckDB's double division gives each its own last bits; the client's text is the exact program's
|
|
1902
|
+
if (a0 === NUMERIC && a1 === NUMERIC)
|
|
1903
|
+
return fn("__mason_numeric_div", [cast(b.lexpr, ["varchar"]), cast(b.rexpr, ["varchar"])]);
|
|
1904
|
+
// a float division by zero is Postgres's error, where DuckDB answers Infinity or NULL
|
|
1905
|
+
return op("/", b.lexpr, nonZero(this.cp, b.rexpr));
|
|
1906
|
+
}
|
|
1907
|
+
case "%": return null;
|
|
1908
|
+
case "#": return a0 !== null && INTEGERS.has(a0) && l !== undefined && r !== undefined ? fn("xor", [b.lexpr, b.rexpr]) : null;
|
|
1909
|
+
case "<<":
|
|
1910
|
+
case ">>": {
|
|
1911
|
+
// C's shift, as Postgres does it: the count masked to the word (31 for a smallint promoted to int, 63 for a
|
|
1912
|
+
// bigint), a left shift wrapping to the type's width. DuckDB refuses to left-shift a negative number
|
|
1913
|
+
if (a0 === null || !INTEGERS.has(a0) || l === undefined || r === undefined)
|
|
1914
|
+
return null;
|
|
1915
|
+
const count = op("&", b.rexpr, intConst(a0 === INT8 ? 63 : 31));
|
|
1916
|
+
const type = a0 === INT8 ? "bigint" : a0 === INT4 ? "integer" : "smallint";
|
|
1917
|
+
if (name === ">>")
|
|
1918
|
+
return cast(op(">>", b.lexpr, count), [type]);
|
|
1919
|
+
const bits = a0 === INT8 ? 64 : a0 === INT4 ? 32 : 16;
|
|
1920
|
+
const huge = (v) => cast(strConst(String(v)), ["hugeint"]);
|
|
1921
|
+
const product = op("*", cast(b.lexpr, ["hugeint"]), op("<<", huge(1n), count));
|
|
1922
|
+
const half = huge(1n << BigInt(bits - 1)), whole = huge(1n << BigInt(bits));
|
|
1923
|
+
return cast(op("-", op("%", op("+", op("%", op("+", product, half), whole), whole), whole), half), [type]);
|
|
1924
|
+
}
|
|
1925
|
+
case "*": {
|
|
1926
|
+
// a numeric product is exact to 38 digits in Postgres; DuckDB multiplies two narrow DECIMALs in 18
|
|
1927
|
+
if (t?.type !== NUMERIC || l === undefined || r === undefined)
|
|
1928
|
+
return null;
|
|
1929
|
+
const wide = (x) => { const m = this.typed(x)?.typmod ?? -1; return m >= 4 && this.typeOf(x) === NUMERIC ? cast(x, ["decimal", "38", String((m - 4) & 0xffff)]) : x; };
|
|
1930
|
+
b.lexpr = wide(b.lexpr);
|
|
1931
|
+
b.rexpr = wide(b.rexpr);
|
|
1932
|
+
return null;
|
|
1933
|
+
}
|
|
1934
|
+
case "-": {
|
|
1935
|
+
if (a0 === JSONB && l !== undefined && r !== undefined) {
|
|
1936
|
+
if (a1 !== null && TEXTS.has(a1))
|
|
1937
|
+
return fn("__mason_jsonb_delete", [jsonArg(l), r]);
|
|
1938
|
+
throw new Unrewritable("jsonb - of a text array or an index is not supported by Mason", "build the value with jsonb_build_object", "jsonb-set");
|
|
1939
|
+
}
|
|
1940
|
+
// a date minus a date is Postgres's integer (date_mi), DuckDB's BIGINT: a date plus it has no DuckDB function
|
|
1941
|
+
if (a0 === DATE && a1 === DATE && l !== undefined && r !== undefined)
|
|
1942
|
+
return cast(op("-", b.lexpr, b.rexpr), ["integer"]);
|
|
1943
|
+
// a time minus a time: as timestamps of one day (DuckDB has no TIME - TIME)
|
|
1944
|
+
if (a0 === TIME && a1 === TIME && l !== undefined && r !== undefined)
|
|
1945
|
+
return op("-", op("+", cast(strConst("2000-01-01"), ["date"]), l), op("+", cast(strConst("2000-01-01"), ["date"]), r));
|
|
1946
|
+
return null;
|
|
1947
|
+
}
|
|
1948
|
+
case "#-": throw new Unrewritable("jsonb #- is not supported by Mason", "build the value with jsonb_build_object", "jsonb-set");
|
|
1949
|
+
case "||": {
|
|
1950
|
+
if (t?.type === JSONB && l !== undefined && r !== undefined)
|
|
1951
|
+
return fn("__mason_jsonb_concat", [jsonArg(l), jsonArg(r)]);
|
|
1952
|
+
// text || a value (textanycat, anytextcat): the value's text is its output function's
|
|
1953
|
+
// an array and an element or another array: DuckDB's || joins two of one type only
|
|
1954
|
+
if (t !== null && ARRAYS.has(t.type) && l !== undefined && r !== undefined) {
|
|
1955
|
+
const la = a0 !== null && ARRAYS.has(a0), ra = a1 !== null && ARRAYS.has(a1);
|
|
1956
|
+
return la && ra ? caseWhen([[and([isNull(this.clone(l)), isNull(this.clone(r))]), nullConst()]], fn("list_concat", [l, r]))
|
|
1957
|
+
: la ? fn("list_append", [l, r]) : fn("list_prepend", [l, r]);
|
|
1958
|
+
}
|
|
1959
|
+
if (t === null || !TEXTS.has(t.type))
|
|
1960
|
+
return null;
|
|
1961
|
+
// textanycat is `$1::text || $2`: the value's text is its cast's (`true`, where its output is `t`)
|
|
1962
|
+
if (l !== undefined && a0 !== null && !TEXTS.has(a0))
|
|
1963
|
+
b.lexpr = this.pgText(l, a0, true) ?? l;
|
|
1964
|
+
if (r !== undefined && a1 !== null && !TEXTS.has(a1))
|
|
1965
|
+
b.rexpr = this.pgText(r, a1, true) ?? r;
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
case "~":
|
|
1969
|
+
case "~*":
|
|
1970
|
+
case "!~":
|
|
1971
|
+
case "!~*": {
|
|
1972
|
+
if (l === undefined || r === undefined)
|
|
1973
|
+
return null;
|
|
1974
|
+
const re = regexPattern(r);
|
|
1975
|
+
// a pattern RE2 would read otherwise (regexPattern): Postgres's engine, a chunk at a time
|
|
1976
|
+
if (re === null)
|
|
1977
|
+
return pgCall("bool", `a1 ${name} a2`, [l, r]);
|
|
1978
|
+
const call = fn("regexp_matches", [l, re, ...(name.endsWith("*") ? [strConst("i")] : [])]);
|
|
1979
|
+
return name.startsWith("!") ? not(call) : call;
|
|
1980
|
+
}
|
|
1981
|
+
case "#>":
|
|
1982
|
+
case "#>>":
|
|
1983
|
+
case "->":
|
|
1984
|
+
case "->>": {
|
|
1985
|
+
if (a0 === null || !JSONS.has(a0))
|
|
1986
|
+
return null;
|
|
1987
|
+
const either = name.startsWith("#"), canonical = a0 === JSONB, asText = name.endsWith(">>");
|
|
1988
|
+
let steps = either ? jsonPath(r) : jsonSteps([r], a1 !== null && INTEGERS.has(a1)), base = l;
|
|
1989
|
+
// `(c -> 'a') ->> 'b'` is one read at the steps a, b (jsonChain)
|
|
1990
|
+
const chain = this.jsonChain(l, either), literal = constString(steps);
|
|
1991
|
+
if (chain !== null && literal !== null) {
|
|
1992
|
+
base = chain.base;
|
|
1993
|
+
steps = strConst(JSON.stringify([...chain.steps, ...JSON.parse(literal)]));
|
|
1994
|
+
}
|
|
1995
|
+
// a stored column of the path where every row holds it (keptRead), else DuckDB's own read where it is
|
|
1996
|
+
// Postgres's, else the function (jsonGet)
|
|
1997
|
+
return this.jsonRead(base, steps, either, canonical, asText);
|
|
1998
|
+
}
|
|
1999
|
+
case "@>":
|
|
2000
|
+
case "<@":
|
|
2001
|
+
if (a0 !== null && JSONS.has(a0))
|
|
2002
|
+
return name === "@>" ? fn("__mason_jsonb_contains", [jsonArg(l), jsonArg(r)]) : fn("__mason_jsonb_contains", [jsonArg(r), jsonArg(l)]);
|
|
2003
|
+
if (a0 === null || ARRAY_DUCK[a0] === undefined && NUMBER_ELEMENT[a0] === undefined || l === undefined || r === undefined)
|
|
2004
|
+
return null;
|
|
2005
|
+
{
|
|
2006
|
+
// an array's NULL element is contained in nothing in Postgres ('{NULL,3}' <@ '{1,2,3}' is false); DuckDB
|
|
2007
|
+
// finds it contained
|
|
2008
|
+
const inner = name === "@>" ? b.rexpr : b.lexpr;
|
|
2009
|
+
return { BoolExpr: { boolop: "AND_EXPR", args: [{ A_Expr: { ...b } }, op("=", fn("list_count", [this.cp(inner)]), fn("len", [this.cp(inner)]))] } };
|
|
2010
|
+
}
|
|
2011
|
+
case "?":
|
|
2012
|
+
case "?|":
|
|
2013
|
+
case "?&": {
|
|
2014
|
+
if (a0 === null || !JSONS.has(a0))
|
|
2015
|
+
return null;
|
|
2016
|
+
const f = { "?": "__mason_jsonb_exists", "?|": "__mason_jsonb_exists_any", "?&": "__mason_jsonb_exists_all" }[name];
|
|
2017
|
+
return fn(f, [jsonArg(l), r]);
|
|
2018
|
+
}
|
|
2019
|
+
case "<":
|
|
2020
|
+
case ">":
|
|
2021
|
+
case "<=":
|
|
2022
|
+
case ">=": {
|
|
2023
|
+
if (l === undefined || r === undefined)
|
|
2024
|
+
return null;
|
|
2025
|
+
// the operands as rewritten so far (a citext lower-cased to text sorts under the collation)
|
|
2026
|
+
const c = this.textCollation(b.lexpr) ?? this.textCollation(b.rexpr);
|
|
2027
|
+
if (c === null || c.kind === "bytes" || (t !== null && BYTE_COLLATIONS.has(t.inputCollation)))
|
|
2028
|
+
return null;
|
|
2029
|
+
// the operands as Postgres cast them (a char(n) beside a text is its text without the padding)
|
|
2030
|
+
return this.compareText(name, b.lexpr, b.rexpr, c);
|
|
2031
|
+
}
|
|
2032
|
+
default: return null;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
widen(n, want) {
|
|
2036
|
+
if (n === undefined || want === null || want === undefined || this.widened.get(bodyOf(n)) === want)
|
|
2037
|
+
return n;
|
|
2038
|
+
const made = this.widenOnce(n, want);
|
|
2039
|
+
if (made !== n)
|
|
2040
|
+
this.widened.set(bodyOf(made), want);
|
|
2041
|
+
return made;
|
|
2042
|
+
}
|
|
2043
|
+
widenOnce(n, want) {
|
|
2044
|
+
if ((want === TEXT || want === VARCHAR) && this.typeOf(n) === BPCHAR) {
|
|
2045
|
+
const r = fn("rtrim", [n]);
|
|
2046
|
+
this.typedLike.set(bodyOf(r), n);
|
|
2047
|
+
return r;
|
|
2048
|
+
}
|
|
2049
|
+
// a date (or a timestamp) where Postgres chose a timestamptz's function, generate_series(date, date, interval)
|
|
2050
|
+
// among them: cast in the session's zone, as Postgres's implicit cast is (DuckDB would keep a timestamp)
|
|
2051
|
+
if (want === TIMESTAMPTZ || want === TIMESTAMP) {
|
|
2052
|
+
const own = this.typeOf(n);
|
|
2053
|
+
// a timestamptz Postgres chose is cast whatever the argument's own type reads as: an implicit cast of a constant
|
|
2054
|
+
// has no offset of its own, so a date literal can read as the timestamptz it became
|
|
2055
|
+
if (want === TIMESTAMPTZ)
|
|
2056
|
+
return own === TIMESTAMPTZ && kindOf(n) !== "TypeCast" && kindOf(n) !== "A_Const" ? n : cast(n, ["timestamptz"]);
|
|
2057
|
+
if (own === DATE)
|
|
2058
|
+
return cast(n, ["timestamp"]);
|
|
2059
|
+
return n;
|
|
2060
|
+
}
|
|
2061
|
+
if (want === NUMERIC || !NUMBER_TYPES.has(want))
|
|
2062
|
+
return n;
|
|
2063
|
+
const own = this.typeOf(n);
|
|
2064
|
+
if (own === null || own === want || !NUMBER_TYPES.has(own))
|
|
2065
|
+
return n;
|
|
2066
|
+
const type = duckTypeOf(want);
|
|
2067
|
+
return type === null ? n : cast(n, type);
|
|
2068
|
+
}
|
|
2069
|
+
/** a call kept as written: each argument as the type Postgres resolved it to (an ordered-set aggregate's sorted one) */
|
|
2070
|
+
widenArgs(b) {
|
|
2071
|
+
const t = this.typed({ FuncCall: b });
|
|
2072
|
+
if (t === null)
|
|
2073
|
+
return;
|
|
2074
|
+
if (b.agg_within_group === true) {
|
|
2075
|
+
for (const [i, s] of (b.agg_order ?? []).entries()) {
|
|
2076
|
+
const sb = bodyOf(s);
|
|
2077
|
+
if (isNode(sb.node))
|
|
2078
|
+
sb.node = this.widen(sb.node, t.args[i]?.type);
|
|
2079
|
+
}
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
if (Array.isArray(b.args))
|
|
2083
|
+
b.args = b.args.map((a, i) => this.widen(a, t.args[i]?.type));
|
|
2084
|
+
}
|
|
2085
|
+
// --------------------------------------------------------------------------------------------- functions
|
|
2086
|
+
funcCall(b) {
|
|
2087
|
+
const name = lastName(b.funcname);
|
|
2088
|
+
const t = this.typed({ FuncCall: b });
|
|
2089
|
+
const args = (b.args ?? []);
|
|
2090
|
+
const argType = (i) => t?.args[i]?.type ?? (args[i] !== undefined ? this.typeOf(args[i]) : null);
|
|
2091
|
+
// each argument as the type Postgres resolved it to (a real to a double precision, a char(n) to its text), before
|
|
2092
|
+
// any rule reads it; an aggregate's own ORDER BY of the same expression with it (DISTINCT needs them alike)
|
|
2093
|
+
if (b.agg_within_group !== true) {
|
|
2094
|
+
args.forEach((a, i) => {
|
|
2095
|
+
const w = this.widen(a, t?.args[i]?.type);
|
|
2096
|
+
if (w === a)
|
|
2097
|
+
return;
|
|
2098
|
+
args[i] = w;
|
|
2099
|
+
for (const s of (b.agg_order ?? []))
|
|
2100
|
+
if (isNode(bodyOf(s).node) && shapeOf(bodyOf(s).node) === shapeOf(a))
|
|
2101
|
+
bodyOf(s).node = this.widen(bodyOf(s).node, t?.args[i]?.type);
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
// an array of arrays is a multi-dimensional array, which DuckDB's lists are not
|
|
2105
|
+
if ((name === "array_agg" || name === "array_cat") && args[0] !== undefined && argType(0) !== null && ARRAYS.has(argType(0)) && name === "array_agg") {
|
|
2106
|
+
throw new Unrewritable("array_agg of an array (a multi-dimensional array) is not supported by Mason", "array_agg the elements, or array_to_string the arrays first", "multidim-array");
|
|
2107
|
+
}
|
|
2108
|
+
if ((name === "sum" || name === "avg") && argType(0) === INTERVAL)
|
|
2109
|
+
throw new Unrewritable(`${name}() of an interval is not supported by Mason`, "sum(extract(epoch FROM x)) for the seconds", "interval-aggregates");
|
|
2110
|
+
if (name === "sum" && this.typeOf(args[0] ?? { A_Const: {} }) === FLOAT4 && argType(0) === FLOAT4)
|
|
2111
|
+
throw new Unrewritable("sum() of a real is not supported by Mason (Postgres adds in single precision)", "sum(x::double precision)", "sum-real");
|
|
2112
|
+
if (b.agg_distinct === true && args[0] !== undefined)
|
|
2113
|
+
this.comparable(args[0], `${name}(DISTINCT …) of`);
|
|
2114
|
+
// a function-style cast (`date(x)`, `text(x)`, `numeric(x)`) is the cast
|
|
2115
|
+
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
|
+
if (t.type === TEXT || t.type === VARCHAR) {
|
|
2117
|
+
const text = this.pgText(args[0], this.typeOf(args[0]), true);
|
|
2118
|
+
if (text !== null)
|
|
2119
|
+
return text;
|
|
2120
|
+
}
|
|
2121
|
+
const type = duckTypeOf(t.type, t.typmod);
|
|
2122
|
+
if (type !== null && !(t.type === NUMERIC && t.typmod < 0))
|
|
2123
|
+
return cast(args[0], type);
|
|
2124
|
+
}
|
|
2125
|
+
// an aggregate over DISTINCT values sorts them to find the duplicates, so Postgres's string_agg / array_agg (DISTINCT)
|
|
2126
|
+
// lists them in that order (the argument's, under its collation); DuckDB's in any order
|
|
2127
|
+
if (b.agg_distinct === true && b.agg_order === undefined && args[0] !== undefined && ["string_agg", "array_agg", "json_agg", "jsonb_agg"].includes(name)) {
|
|
2128
|
+
b.agg_order = [{ SortBy: { node: this.clone(args[0]), sortby_dir: "SORTBY_DEFAULT", sortby_nulls: "SORTBY_NULLS_DEFAULT" } }];
|
|
2129
|
+
this.original.add(bodyOf(b.agg_order[0]));
|
|
2130
|
+
}
|
|
2131
|
+
if (b.agg_within_group === true)
|
|
2132
|
+
return this.withinGroup(name, b);
|
|
2133
|
+
switch (name) {
|
|
2134
|
+
case "every":
|
|
2135
|
+
b.funcname = [S("bool_and")];
|
|
2136
|
+
return null;
|
|
2137
|
+
case "upper":
|
|
2138
|
+
case "lower": {
|
|
2139
|
+
if (args.length !== 1 || argType(0) === null || !TEXTS.has(argType(0)))
|
|
2140
|
+
return null;
|
|
2141
|
+
// glibc's upper keeps ß (DuckDB's gives ẞ); its every other letter is DuckDB's (an exhaustive comparison over
|
|
2142
|
+
// every code point differed only in letters newer than the server's glibc)
|
|
2143
|
+
if (this.caseMode === "libc")
|
|
2144
|
+
return name === "lower" ? null : fn("replace", [fn("upper", [fn("replace", [args[0], strConst("ß"), strConst("\ufdd0")])]), strConst("\ufdd0"), strConst("ß")]);
|
|
2145
|
+
return fn("__mason_case", [args[0], boolConst(name === "upper"), strConst(this.caseMode)]);
|
|
2146
|
+
}
|
|
2147
|
+
case "initcap": return args.length === 1 ? fn("__mason_initcap", [args[0], strConst(this.caseMode)]) : null;
|
|
2148
|
+
case "btrim":
|
|
2149
|
+
b.funcname = [S("trim")];
|
|
2150
|
+
b.funcformat = "COERCE_EXPLICIT_CALL";
|
|
2151
|
+
return null;
|
|
2152
|
+
case "to_hex":
|
|
2153
|
+
// an integer's two's complement in its own width, in lower case
|
|
2154
|
+
if (args.length === 1 && argType(0) === INT4)
|
|
2155
|
+
return fn("lower", [fn("to_hex", [op("&", cast(args[0], ["bigint"]), numConst("4294967295"))])]);
|
|
2156
|
+
return args.length === 1 ? fn("lower", [fn("to_hex", [args[0]])]) : null;
|
|
2157
|
+
case "array_remove": {
|
|
2158
|
+
// by the elements' text (src/duck/functions.ts), the answer cast back to the array's type
|
|
2159
|
+
const element = ARRAY_DUCK[t?.type ?? -1];
|
|
2160
|
+
if (element === undefined || args.length !== 2)
|
|
2161
|
+
throw new Unrewritable(`array_remove() of an array of type OID ${t?.type ?? "unknown"} is not supported by Mason`, "array_remove an array of text, integers, floats, booleans, dates or timestamps", "array-remove-type");
|
|
2162
|
+
return arrayCast(fn("__mason_array_remove", [arrayCast(args[0], "varchar"), cast(args[1], ["varchar"])]), element);
|
|
2163
|
+
}
|
|
2164
|
+
case "overlaps": {
|
|
2165
|
+
if (args.length !== 4)
|
|
2166
|
+
return null;
|
|
2167
|
+
// (start, length) is the period start … start + length
|
|
2168
|
+
const ends = args.map((a, i) => i % 2 === 1 && this.typeOf(a) === INTERVAL ? op("+", this.clone(args[i - 1]), a) : a);
|
|
2169
|
+
const [a1, b1, a2, b2] = ends.map((a, i) => i % 2 === 0 || this.typeOf(args[i]) !== INTERVAL ? this.asInstant(a, t?.args[i]?.type ?? null) : a);
|
|
2170
|
+
return overlaps(this.cp, a1, b1, a2, b2);
|
|
2171
|
+
}
|
|
2172
|
+
case "generate_series":
|
|
2173
|
+
// in a SELECT list, a set of rows: DuckDB's generate_series there is one list, unnest makes it rows
|
|
2174
|
+
if (this.inFrom.has(b))
|
|
2175
|
+
return null;
|
|
2176
|
+
b.args = args.map((a, i) => i < 2 ? this.asInstant(a, argType(i)) : a);
|
|
2177
|
+
return fn("unnest", [{ FuncCall: b }]);
|
|
2178
|
+
case "regexp_match":
|
|
2179
|
+
case "regexp_matches": return this.regexpMatch(name, b, args);
|
|
2180
|
+
case "round":
|
|
2181
|
+
if (args.length === 1 && argType(0) !== null && FLOATS.has(argType(0)))
|
|
2182
|
+
return fn("round_even", [args[0], intConst(0)]);
|
|
2183
|
+
return null;
|
|
2184
|
+
case "timezone":
|
|
2185
|
+
// AT TIME ZONE z: the zone as Postgres reads it (zoneArg); the call kept
|
|
2186
|
+
if (args.length === 2)
|
|
2187
|
+
b.args = [this.zoneArg(args[0]), args[1]];
|
|
2188
|
+
return null;
|
|
2189
|
+
case "date_trunc":
|
|
2190
|
+
if (args.length === 3) {
|
|
2191
|
+
// in a zone: the wall clock there truncated, read back as an instant in that zone
|
|
2192
|
+
const [unit, ts] = args;
|
|
2193
|
+
const zone = this.zoneArg(args[2]);
|
|
2194
|
+
return fn("timezone", [zone, fn("date_trunc", [unit, fn("timezone", [this.clone(zone), this.asInstant(ts, argType(1))])])]);
|
|
2195
|
+
}
|
|
2196
|
+
if (args.length !== 2)
|
|
2197
|
+
return null;
|
|
2198
|
+
{
|
|
2199
|
+
const unit = (constString(args[0]) ?? "").toLowerCase();
|
|
2200
|
+
const own = this.typeOf(args[1]);
|
|
2201
|
+
if (own === TIME)
|
|
2202
|
+
throw new Unrewritable("date_trunc() of a time is not supported by Mason", "date_trunc a timestamp, or extract the parts", "time-date-trunc");
|
|
2203
|
+
if (argType(1) === INTERVAL && unit === "week")
|
|
2204
|
+
throw new Unrewritable(`unit "week" not supported for type interval`, "", { postgres: "0A000" });
|
|
2205
|
+
const x = argType(1) === TIMESTAMPTZ && own === DATE ? cast(args[1], ["timestamptz"]) : args[1];
|
|
2206
|
+
// Postgres's centuries and millennia begin in a year 1 (2001-01-01), DuckDB's in a year 0 (DT_CENTURY)
|
|
2207
|
+
// (and a decade before year 1 is counted back as Postgres counts it: timestamp_trunc's DTK_DECADE)
|
|
2208
|
+
if ((unit === "century" || unit === "millennium" || unit === "decade") && argType(1) !== INTERVAL) {
|
|
2209
|
+
const y = fn("year", [this.clone(x)]);
|
|
2210
|
+
const first = unit === "decade"
|
|
2211
|
+
? 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)))
|
|
2212
|
+
: (() => {
|
|
2213
|
+
const n = unit === "century" ? 100 : 1000;
|
|
2214
|
+
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)));
|
|
2215
|
+
})();
|
|
2216
|
+
const truncated = op("+", fn("date_trunc", [strConst("year"), this.clone(x)]), fn("to_years", [cast(op("-", first, this.clone(y)), ["integer"])]));
|
|
2217
|
+
return caseWhen([[fn("isfinite", [this.clone(x)]), truncated]], argType(1) === TIMESTAMPTZ ? cast(x, ["timestamptz"]) : cast(x, ["timestamp"]));
|
|
2218
|
+
}
|
|
2219
|
+
return x === args[1] ? null : fn("date_trunc", [args[0], x]);
|
|
2220
|
+
}
|
|
2221
|
+
case "make_interval": return makeInterval(args);
|
|
2222
|
+
// isfinite of a time or an interval: DuckDB has none, and neither is ever infinite here
|
|
2223
|
+
case "isfinite": {
|
|
2224
|
+
const own = args[0] === undefined ? null : this.typeOf(args[0]);
|
|
2225
|
+
return own === TIME || own === INTERVAL ? caseWhen([[isNull(args[0]), nullConst()]], boolConst(true)) : null;
|
|
2226
|
+
}
|
|
2227
|
+
case "date_bin": return args.length === 3 ? fn("time_bucket", args) : null;
|
|
2228
|
+
case "justify_days":
|
|
2229
|
+
case "justify_hours":
|
|
2230
|
+
case "justify_interval": return args.length === 1 ? fn("__mason_justify", [strConst(name.slice("justify_".length)), args[0]]) : null;
|
|
2231
|
+
case "rpad":
|
|
2232
|
+
case "lpad":
|
|
2233
|
+
if (args.length === 2)
|
|
2234
|
+
b.args = [...args, strConst(" ")];
|
|
2235
|
+
return null;
|
|
2236
|
+
case "chr":
|
|
2237
|
+
if (args.length === 1)
|
|
2238
|
+
b.args = [cast(args[0], ["integer"])];
|
|
2239
|
+
return null;
|
|
2240
|
+
case "quote_literal": return args.length === 1 ? fn("__mason_quote_literal", [this.pgText(args[0], argType(0), true) ?? cast(args[0], ["varchar"])]) : null;
|
|
2241
|
+
case "quote_ident": throw new Unrewritable("quote_ident() is not supported by Mason", "quote the identifier in the statement", "quote-ident");
|
|
2242
|
+
case "length":
|
|
2243
|
+
case "char_length":
|
|
2244
|
+
case "character_length":
|
|
2245
|
+
// a bytea's length is its bytes (DuckDB has no length of a BLOB)
|
|
2246
|
+
if (args.length === 1 && argType(0) === BYTEA)
|
|
2247
|
+
return fn("octet_length", [args[0]]);
|
|
2248
|
+
return args.length === 1 && argType(0) === BPCHAR ? fn("length", [fn("rtrim", [args[0]])]) : null;
|
|
2249
|
+
// an empty array has no dimension 1 (Postgres's NULL; DuckDB has neither function)
|
|
2250
|
+
case "array_upper":
|
|
2251
|
+
case "array_lower": {
|
|
2252
|
+
if (args.length !== 2)
|
|
2253
|
+
return null;
|
|
2254
|
+
if (constInt(args[1]) !== 1)
|
|
2255
|
+
return constInt(args[1]) === null ? null : nullConst();
|
|
2256
|
+
return caseWhen([[op(">", fn("len", [this.clone(args[0])]), intConst(0)), name === "array_upper" ? fn("len", [args[0]]) : intConst(1)]], null);
|
|
2257
|
+
}
|
|
2258
|
+
case "array_cat": return args.length === 2 ? caseWhen([[and([isNull(this.clone(args[0])), isNull(this.clone(args[1]))]), nullConst()]], fn("list_concat", args)) : null;
|
|
2259
|
+
case "array_to_string": return args.length === 2 || args.length === 3 ? fn("__mason_array_to_string", [arrayCast(args[0], "varchar"), args[1], args[2] ?? cast(nullConst(), ["varchar"])]) : null;
|
|
2260
|
+
case "jsonb_strip_nulls": return args.length === 1 ? fn("__mason_jsonb_strip_nulls", [jsonArg(args[0])]) : null;
|
|
2261
|
+
case "jsonb_set":
|
|
2262
|
+
case "jsonb_insert":
|
|
2263
|
+
case "jsonb_set_lax": throw new Unrewritable(`${name}() is not supported by Mason`, "build the value with jsonb_build_object", "jsonb-set");
|
|
2264
|
+
case "div": {
|
|
2265
|
+
// div of two integers is their integer quotient, truncated toward zero
|
|
2266
|
+
const own = args.map((a) => this.typeOf(a));
|
|
2267
|
+
if (args.length === 2 && own.every((x) => x !== null && INTEGERS.has(x)))
|
|
2268
|
+
return cast(op("//", args[0], nonZero(this.cp, args[1])), ["decimal", "38", "0"]);
|
|
2269
|
+
throw new Unrewritable("div() of a value that is not an integer is not supported by Mason", "trunc(a / b)", "div-numeric");
|
|
2270
|
+
}
|
|
2271
|
+
case "width_bucket": {
|
|
2272
|
+
// width_bucket_float8 / _numeric: below the range 0, at or past its top count + 1, else the bucket
|
|
2273
|
+
if (args.length !== 4 || (argType(0) !== FLOAT8 && argType(0) !== NUMERIC))
|
|
2274
|
+
throw new Unrewritable("width_bucket() of an array of thresholds is not supported by Mason", "width_bucket(value, low, high, count)", "width-bucket-forms");
|
|
2275
|
+
const [x, lo, hi, n] = args.map((a) => argType(0) === FLOAT8 ? cast(a, ["double"]) : a);
|
|
2276
|
+
const count = cast(args[3], ["integer"]);
|
|
2277
|
+
const bucket = op("+", fn("least", [cast(fn("trunc", [op("*", n, op("/", op("-", x, lo), op("-", hi, this.clone(lo))))]), ["integer"]), op("-", this.clone(count), intConst(1))]), intConst(1));
|
|
2278
|
+
return caseWhen([
|
|
2279
|
+
[or(args.map((a) => isNull(this.clone(a)))), nullConst()],
|
|
2280
|
+
[op(">=", this.clone(lo), this.clone(hi)), fn("error", [strConst(`mason: width_bucket() with its bounds reversed or equal is not supported by Mason (${"Mason gap width-bucket-forms"})`)])],
|
|
2281
|
+
[op("<", this.clone(x), this.clone(lo)), intConst(0)],
|
|
2282
|
+
[op(">=", this.clone(x), this.clone(hi)), op("+", this.clone(count), intConst(1))],
|
|
2283
|
+
], bucket);
|
|
2284
|
+
}
|
|
2285
|
+
case "extract": return this.extract(args);
|
|
2286
|
+
case "date_part": return this.datePart(args);
|
|
2287
|
+
case "jsonb_typeof":
|
|
2288
|
+
case "json_typeof": return jsonTypeof(args[0]);
|
|
2289
|
+
case "jsonb_array_length":
|
|
2290
|
+
case "json_array_length": return args.length === 1 ? fn("__mason_json_array_length", [jsonArg(args[0])]) : null;
|
|
2291
|
+
// DuckDB's string_split makes [''] of '' and characters of an empty delimiter (src/duck/functions.ts stringToArray)
|
|
2292
|
+
case "string_to_array": return args.length === 2 || args.length === 3 ? fn("__mason_string_to_array", [args[0], args[1], args[2] ?? cast(nullConst(), ["varchar"])]) : null;
|
|
2293
|
+
// an empty array has no dimension 1 (Postgres's NULL; DuckDB's 0)
|
|
2294
|
+
case "array_length": {
|
|
2295
|
+
if (args.length !== 2 || constInt(args[1]) !== 1)
|
|
2296
|
+
return null;
|
|
2297
|
+
return caseWhen([[op("=", fn("len", [this.clone(args[0])]), intConst(0)), nullConst()]], fn("array_length", args));
|
|
2298
|
+
}
|
|
2299
|
+
// array_to_json(a) is to_json(a): one writer, so an array's elements are written by their type (a jsonb's embedded)
|
|
2300
|
+
case "array_to_json":
|
|
2301
|
+
case "jsonb_build_object":
|
|
2302
|
+
case "json_build_object":
|
|
2303
|
+
case "jsonb_build_array":
|
|
2304
|
+
case "json_build_array":
|
|
2305
|
+
case "to_json":
|
|
2306
|
+
case "to_jsonb": {
|
|
2307
|
+
if (name === "array_to_json" && args.length !== 1)
|
|
2308
|
+
return null;
|
|
2309
|
+
const values = args.map((a, i) => this.jsonValue(a, t?.args[i]?.type ?? this.typeOf(a), name));
|
|
2310
|
+
const style = name.endsWith("_object") ? "o" : name.endsWith("_array") ? "a" : "v";
|
|
2311
|
+
return fn("__mason_json_build", [strConst(`${style}:${values.map((v) => v.kind).join("")}`), ...values.map((v) => v.value)]);
|
|
2312
|
+
}
|
|
2313
|
+
case "jsonb_agg":
|
|
2314
|
+
case "json_agg": {
|
|
2315
|
+
if (b.agg_distinct === true)
|
|
2316
|
+
throw new Unrewritable(`${name}(DISTINCT …) is not supported by Mason`, `select the DISTINCT values in a subquery and ${name} them`, "json-agg-distinct");
|
|
2317
|
+
if (b.over !== undefined || args.length !== 1)
|
|
2318
|
+
return null;
|
|
2319
|
+
const isArray = name === "json_agg" && argType(0) !== null && ARRAYS.has(argType(0));
|
|
2320
|
+
const tested = isArray ? this.cp(args[0]) : null;
|
|
2321
|
+
const v = this.jsonValue(args[0], argType(0), name);
|
|
2322
|
+
const item = fn("__mason_json_value", [strConst(v.kind), v.value]);
|
|
2323
|
+
// json_agg puts a line break before each array but the first, and never before a NULL (json_agg_transfn);
|
|
2324
|
+
// jsonb_agg's text is canonical
|
|
2325
|
+
if (tested !== null)
|
|
2326
|
+
return linedAgg(b, caseWhen([[isNull(tested), item]], concat(strConst("\n "), this.cp(item))), this.done);
|
|
2327
|
+
const agg = { FuncCall: { ...b, funcname: [S("string_agg")], args: [item, strConst(", ")] } };
|
|
2328
|
+
this.done.add(bodyOf(agg));
|
|
2329
|
+
return concat(strConst("["), agg, strConst("]"));
|
|
2330
|
+
}
|
|
2331
|
+
case "jsonb_object_agg":
|
|
2332
|
+
case "json_object_agg": {
|
|
2333
|
+
if (b.over !== undefined || args.length !== 2 || b.agg_distinct === true)
|
|
2334
|
+
return null;
|
|
2335
|
+
const v = this.jsonValue(args[1], argType(1), name);
|
|
2336
|
+
const member = concat(fn("__mason_json_value", [strConst("s"), cast(args[0], ["varchar"])]), strConst(" : "), fn("__mason_json_value", [strConst(v.kind), v.value]));
|
|
2337
|
+
const agg = { FuncCall: { ...b, funcname: [S("string_agg")], args: [member, strConst(", ")] } };
|
|
2338
|
+
this.done.add(bodyOf(agg));
|
|
2339
|
+
return concat(strConst("{ "), agg, strConst(" }"));
|
|
2340
|
+
}
|
|
2341
|
+
case "jsonb_extract_path_text":
|
|
2342
|
+
case "json_extract_path_text":
|
|
2343
|
+
case "jsonb_extract_path":
|
|
2344
|
+
case "json_extract_path": {
|
|
2345
|
+
const own = argType(0);
|
|
2346
|
+
// VARIADIC <array>: the array's elements are the path (a literal as array_in reads it)
|
|
2347
|
+
const literal = b.func_variadic === true && args.length === 2 ? constString(args[1]) : null;
|
|
2348
|
+
const steps = b.func_variadic !== true ? jsonSteps(args.slice(1), false)
|
|
2349
|
+
: literal !== null ? strConst(JSON.stringify(arrayLiteral(literal)))
|
|
2350
|
+
: cast(fn("to_json", [arrayCast(args[1], "varchar")]), ["varchar"]);
|
|
2351
|
+
const canonical = own === JSONB || name.startsWith("jsonb");
|
|
2352
|
+
return this.jsonRead(args[0], steps, true, canonical, name.endsWith("_text"));
|
|
2353
|
+
}
|
|
2354
|
+
case "jsonb_object_keys":
|
|
2355
|
+
case "json_object_keys":
|
|
2356
|
+
if (this.inFrom.has(b))
|
|
2357
|
+
return null;
|
|
2358
|
+
return fn("unnest", [fn("__mason_json_keys", [args[0], boolConst(name.startsWith("jsonb"))])]);
|
|
2359
|
+
case "jsonb_array_elements":
|
|
2360
|
+
case "json_array_elements":
|
|
2361
|
+
case "jsonb_array_elements_text":
|
|
2362
|
+
case "json_array_elements_text":
|
|
2363
|
+
case "jsonb_each":
|
|
2364
|
+
case "json_each":
|
|
2365
|
+
case "jsonb_each_text":
|
|
2366
|
+
case "json_each_text":
|
|
2367
|
+
throw new Unrewritable(`${name}() in a SELECT list is not supported by Mason`, `read it in FROM: SELECT … FROM ${name}(…) AS e`, "json-rows-in-select");
|
|
2368
|
+
case "regexp_replace": {
|
|
2369
|
+
if (args.length < 3)
|
|
2370
|
+
return null;
|
|
2371
|
+
const flags = constString(args[3]);
|
|
2372
|
+
const re = regexPattern(args[1], true);
|
|
2373
|
+
if (args.length > 4 || (args[3] !== undefined && (argType(3) === null || !TEXTS.has(argType(3))))) {
|
|
2374
|
+
throw new Unrewritable("regexp_replace() with a start position or a count is not supported by Mason", "write regexp_replace(text, pattern, replacement[, flags]); the flag g replaces every match", "regexp-flags");
|
|
2375
|
+
}
|
|
2376
|
+
// a pattern, a replacement or flags RE2 would read otherwise: Postgres's engine, a chunk at a time
|
|
2377
|
+
if (re === null || constString(args[2]) === null || (args[3] !== undefined && (flags === null || !/^[gi]*$/.test(flags)))) {
|
|
2378
|
+
return pgCall("text", `regexp_replace(${args.map((_, i) => `a${i + 1}`).join(", ")})`, args);
|
|
2379
|
+
}
|
|
2380
|
+
b.args = [args[0], re, replacement(args[2]), ...args.slice(3)];
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
case "regexp_matches":
|
|
2384
|
+
case "regexp_match":
|
|
2385
|
+
case "regexp_split_to_table":
|
|
2386
|
+
case "regexp_split_to_array":
|
|
2387
|
+
case "regexp_count":
|
|
2388
|
+
case "regexp_instr":
|
|
2389
|
+
case "regexp_substr":
|
|
2390
|
+
case "regexp_like":
|
|
2391
|
+
throw new Unrewritable(`${name}() is not supported by Mason`, "test a match with ~ (or ~*), take a part with substring(text from pattern), or replace with regexp_replace", "regexp-functions");
|
|
2392
|
+
case "substring":
|
|
2393
|
+
// substring(text FROM pattern): Postgres's regular-expression form (two text arguments)
|
|
2394
|
+
if (args.length === 2 && argType(1) !== null && TEXTS.has(argType(1))) {
|
|
2395
|
+
if (regexPattern(args[1], true) === null)
|
|
2396
|
+
return pgCall("text", "substring(a1 from a2)", args);
|
|
2397
|
+
return regexSubstring(this.cp, args[0], args[1]);
|
|
2398
|
+
}
|
|
2399
|
+
if (args.length === 3 && argType(1) !== null && TEXTS.has(argType(1)))
|
|
2400
|
+
throw new Unrewritable("substring(… SIMILAR … ESCAPE …) is not supported by Mason", "use substring(text from pattern) with a POSIX regular expression", "similar-to-forms");
|
|
2401
|
+
return null;
|
|
2402
|
+
case "octet_length":
|
|
2403
|
+
if (argType(0) !== null && TEXTS.has(argType(0)))
|
|
2404
|
+
return fn("strlen", args);
|
|
2405
|
+
return null;
|
|
2406
|
+
case "to_char": return toChar(args, argType(0));
|
|
2407
|
+
case "to_timestamp":
|
|
2408
|
+
if (args.length === 2)
|
|
2409
|
+
return cast(fn("strptime", [args[0], strConst(strptimeFormat(args[1]))]), ["timestamptz"]);
|
|
2410
|
+
return null;
|
|
2411
|
+
case "to_date":
|
|
2412
|
+
if (args.length === 2)
|
|
2413
|
+
return cast(fn("strptime", [args[0], strConst(strptimeFormat(args[1]))]), ["date"]);
|
|
2414
|
+
return null;
|
|
2415
|
+
case "format": return formatCall(args, (a, i) => this.pgText(a, argType(i), false) ?? cast(a, ["varchar"]));
|
|
2416
|
+
// each value as its output function writes it (concat_ws's separator is text)
|
|
2417
|
+
case "concat":
|
|
2418
|
+
case "concat_ws":
|
|
2419
|
+
b.args = args.map((a, i) => name === "concat_ws" && i === 0 ? a : this.pgText(a, argType(i), false) ?? a);
|
|
2420
|
+
return null;
|
|
2421
|
+
// an inet's text functions over its stored text (`10.3.6.45/32`)
|
|
2422
|
+
case "host": return argType(0) === INET && args.length === 1 ? fn("regexp_replace", [args[0], strConst("/\\d+$"), strConst("")]) : null;
|
|
2423
|
+
case "masklen": return argType(0) === INET && args.length === 1 ? cast(fn("regexp_extract", [args[0], strConst("/(\\d+)$"), intConst(1)]), ["integer"]) : null;
|
|
2424
|
+
case "family": return argType(0) === INET && args.length === 1
|
|
2425
|
+
? caseWhen([[{ NullTest: { arg: this.cp(args[0]), nulltesttype: "IS_NULL" } }, nullConst()], [fn("contains", [args[0], strConst(":")]), intConst(6)]], intConst(4)) : null;
|
|
2426
|
+
case "abbrev": return argType(0) === INET && args.length === 1 ? inetOut(args[0]) : null;
|
|
2427
|
+
// the transaction's start, as of the execution (sqlValueFunction says why)
|
|
2428
|
+
case "now":
|
|
2429
|
+
case "transaction_timestamp": return args.length === 0 ? this.now("exec") : null;
|
|
2430
|
+
case "age":
|
|
2431
|
+
// age(t): from midnight of the session's day at the transaction's start (Postgres's `age(current_date, t)`),
|
|
2432
|
+
// never DuckDB's own clock
|
|
2433
|
+
if (args.length === 1 && (argType(0) === TIMESTAMP || argType(0) === TIMESTAMPTZ)) {
|
|
2434
|
+
b.args = [cast(cast(this.now("exec"), ["date"]), [argType(0) === TIMESTAMPTZ ? "timestamptz" : "timestamp"]), args[0]];
|
|
2435
|
+
}
|
|
2436
|
+
return null;
|
|
2437
|
+
// jsonpath is Postgres's own language: each row's answer is Postgres's (src/duck/pg-eval.ts)
|
|
2438
|
+
case "jsonb_path_query_array":
|
|
2439
|
+
case "jsonb_path_query_first":
|
|
2440
|
+
case "jsonb_path_exists":
|
|
2441
|
+
case "jsonb_path_match": {
|
|
2442
|
+
if (args.length < 2 || args.length > 4 || args.some((a) => kindOf(a) === "NamedArgExpr"))
|
|
2443
|
+
return null;
|
|
2444
|
+
const call = pgCall(name.endsWith("_exists") || name.endsWith("_match") ? "bool" : "text", `${name}(${args.map((_, i) => `a${i + 1}::${JSONPATH_ARGS[i]}`).join(", ")})`, args);
|
|
2445
|
+
return name.endsWith("_exists") || name.endsWith("_match") ? call : cast(call, ["json"]);
|
|
2446
|
+
}
|
|
2447
|
+
case "jsonb_path_query":
|
|
2448
|
+
if (this.inFrom.has(b))
|
|
2449
|
+
return null;
|
|
2450
|
+
throw new Unrewritable(`${name}() in a SELECT list is not supported by Mason`, `read it in FROM: SELECT … FROM ${name}(…) AS p`, "json-rows-in-select");
|
|
2451
|
+
// a bytea's functions: DuckDB's octet_length, hex and md5 read a BLOB; Postgres's encodings are its own
|
|
2452
|
+
case "encode": {
|
|
2453
|
+
const format = constString(args[1]);
|
|
2454
|
+
if (argType(0) !== BYTEA || args.length !== 2)
|
|
2455
|
+
return null;
|
|
2456
|
+
if (format === "hex")
|
|
2457
|
+
return fn("lower", [fn("hex", [args[0]])]);
|
|
2458
|
+
if (format === "base64" || format === "escape")
|
|
2459
|
+
return fn("__mason_bytea_encode", [fn("lower", [fn("hex", [args[0]])]), strConst(format)]);
|
|
2460
|
+
throw new Unrewritable(`encode() to ${format === null ? "a format that is not a literal" : JSON.stringify(format)} is not supported by Mason`, "encode(x, 'hex'), 'base64' or 'escape'", "bytea-forms");
|
|
2461
|
+
}
|
|
2462
|
+
case "decode": {
|
|
2463
|
+
const format = constString(args[1]);
|
|
2464
|
+
if (args.length !== 2)
|
|
2465
|
+
return null;
|
|
2466
|
+
if (format === "hex" || format === "base64" || format === "escape")
|
|
2467
|
+
return fn("unhex", [fn("__mason_bytea_decode", [cast(args[0], ["varchar"]), strConst(format)])]);
|
|
2468
|
+
throw new Unrewritable(`decode() from ${format === null ? "a format that is not a literal" : JSON.stringify(format)} is not supported by Mason`, "decode(x, 'hex'), 'base64' or 'escape'", "bytea-forms");
|
|
2469
|
+
}
|
|
2470
|
+
case "convert_to":
|
|
2471
|
+
case "convert_from": {
|
|
2472
|
+
const encoding = (constString(args[1]) ?? "").toUpperCase().replace(/[-_]/g, "");
|
|
2473
|
+
if (args.length !== 2 || encoding !== "UTF8")
|
|
2474
|
+
throw new Unrewritable(`${name}() of an encoding other than UTF8 is not supported by Mason`, `${name}(x, 'UTF8')`, "bytea-forms");
|
|
2475
|
+
return fn(name === "convert_to" ? "encode" : "decode", [args[0]]);
|
|
2476
|
+
}
|
|
2477
|
+
case "min":
|
|
2478
|
+
case "max": {
|
|
2479
|
+
if (this.isCitext(argType(0)))
|
|
2480
|
+
throw new Unrewritable(`${name}() of a citext is not supported by Mason (Postgres picks it without regard to case)`, `${name}(x::text), or ${name}(lower(x))`, "citext-forms");
|
|
2481
|
+
if (argType(0) === INET)
|
|
2482
|
+
throw new Unrewritable(`${name}() of an inet is not supported by Mason (Postgres orders addresses by network, then host)`, `${name}(host(x))`, "inet-order");
|
|
2483
|
+
// a text array picks by each element under the collation: refused as its order is (text-array-order)
|
|
2484
|
+
if (args[0] !== undefined)
|
|
2485
|
+
this.comparable(args[0], `${name}() of`);
|
|
2486
|
+
return this.textMinMax(name, b, args);
|
|
2487
|
+
}
|
|
2488
|
+
default: return null;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
/** a value Postgres reads as a timestamptz (a date or a timestamp is cast, as Postgres casts it implicitly) */
|
|
2492
|
+
asInstant(n, wanted) {
|
|
2493
|
+
const own = this.typeOf(n);
|
|
2494
|
+
return wanted === TIMESTAMPTZ && own !== null && own !== TIMESTAMPTZ && own !== UNKNOWN ? cast(n, ["timestamptz"]) : n;
|
|
2495
|
+
}
|
|
2496
|
+
textMinMax(name, b, args) {
|
|
2497
|
+
const arg = args[0];
|
|
2498
|
+
// a window's too (`max(t) OVER (…)`): arg_min / arg_max are DuckDB window aggregates as well
|
|
2499
|
+
if (arg === undefined)
|
|
2500
|
+
return null;
|
|
2501
|
+
const c = this.textCollation(arg);
|
|
2502
|
+
if (c === null || c.kind === "bytes")
|
|
2503
|
+
return null;
|
|
2504
|
+
if (c.kind === "icu") {
|
|
2505
|
+
b.args = [collate(strip(arg), c.name)];
|
|
2506
|
+
return null;
|
|
2507
|
+
}
|
|
2508
|
+
// libc: the value whose key is least (or greatest); a NULL value has no key and is skipped, as min skips it
|
|
2509
|
+
// (the value in three places, each its own copy: the walker rewrites a node in its first place alone)
|
|
2510
|
+
const key = caseWhen([[not(isNull(this.cp(strip(arg)))), row(libcKeys(this.cp, this.cp(strip(arg)), c.icu))]], null);
|
|
2511
|
+
return { FuncCall: { ...b, funcname: [S(name === "min" ? "arg_min" : "arg_max")], args: [strip(arg), key] } };
|
|
2512
|
+
}
|
|
2513
|
+
/** WITHIN GROUP: mode() picks the most frequent value and, of equals, the first in the ORDER BY's order — DuckDB's
|
|
2514
|
+
* mode over the values in that order picks the same (its tie goes to the first it saw); percentile_disc / _cont
|
|
2515
|
+
* take a position in the order, which DuckDB computes by bytes: refused for text under a libc collation */
|
|
2516
|
+
withinGroup(name, b) {
|
|
2517
|
+
const order = (b.agg_order ?? []);
|
|
2518
|
+
const key = order[0] !== undefined ? bodyOf(order[0]).node : undefined;
|
|
2519
|
+
const c = key === undefined ? null : this.textCollation(key);
|
|
2520
|
+
if (c === null || c.kind === "bytes")
|
|
2521
|
+
return null;
|
|
2522
|
+
if (name === "mode" && order.length === 1) {
|
|
2523
|
+
return { FuncCall: { ...b, args: [this.clone(key)], agg_within_group: undefined } };
|
|
2524
|
+
}
|
|
2525
|
+
if (c.kind === "libc")
|
|
2526
|
+
throw new Unrewritable(`${name}() WITHIN GROUP (ORDER BY text) under the database's libc collation is not supported by Mason`, "order by a number, or take the value with ORDER BY … LIMIT in a subquery", "percentile-text-libc");
|
|
2527
|
+
return null;
|
|
2528
|
+
}
|
|
2529
|
+
/** regexp_match: the first match's groups (or the match) as an array, NULL without one; regexp_matches: a row per
|
|
2530
|
+
* match ('g') or for the first, each such an array */
|
|
2531
|
+
regexpMatch(name, b, args) {
|
|
2532
|
+
const pattern = constString(args[1]);
|
|
2533
|
+
const flags = args[2] === undefined ? "" : constString(args[2]);
|
|
2534
|
+
// a pattern or flags RE2 would read otherwise: Postgres's engine, a chunk at a time (regexp_matches's rows, each
|
|
2535
|
+
// match's text[], as JSON)
|
|
2536
|
+
const viaPostgres = () => {
|
|
2537
|
+
const call = `${name}(${args.map((_, i) => `a${i + 1}`).join(", ")})`;
|
|
2538
|
+
if (name === "regexp_match")
|
|
2539
|
+
return pgCall("list", call, args);
|
|
2540
|
+
const rows = pgCall("list", `ARRAY(SELECT to_json(mason_m)::text FROM ${call} AS mason_m)`, args);
|
|
2541
|
+
const matrix = { TypeCast: { arg: arrayCast(rows, "json"), typeName: { names: [S("varchar")], typemod: -1, arrayBounds: [{ Integer: { ival: -1 } }, { Integer: { ival: -1 } }] } } };
|
|
2542
|
+
return fn("unnest", [matrix]);
|
|
2543
|
+
};
|
|
2544
|
+
if (name === "regexp_matches" && this.inFrom.has(b))
|
|
2545
|
+
throw new Unrewritable("regexp_matches() in FROM is not supported by Mason", "call it in the SELECT list: SELECT regexp_matches(…) FROM t", "regexp-matches-forms");
|
|
2546
|
+
if (pattern === null || flags === null || regexPattern(args[1], true) === null || !/^[gi]*$/.test(flags)
|
|
2547
|
+
|| (name === "regexp_matches" && flags.includes("g") && captureGroups(pattern) > 1))
|
|
2548
|
+
return viaPostgres();
|
|
2549
|
+
if (name === "regexp_match" && flags.includes("g"))
|
|
2550
|
+
throw new Unrewritable("regexp_match() does not support the \"global\" option", "use the regexp_matches function instead", { postgres: "22023" });
|
|
2551
|
+
const s = args[0];
|
|
2552
|
+
const re = regexPattern(args[1], true);
|
|
2553
|
+
const options = strConst(flags.includes("i") ? "i" : "");
|
|
2554
|
+
const groups = captureGroups(pattern);
|
|
2555
|
+
const extract = (g) => fn("regexp_extract", [this.clone(s), this.clone(re), intConst(g), this.clone(options)]);
|
|
2556
|
+
const found = arrayOf(groups === 0 ? [extract(0)] : Array.from({ length: groups }, (_, i) => extract(i + 1)));
|
|
2557
|
+
const matches = fn("regexp_matches", [s, re, options]);
|
|
2558
|
+
if (name === "regexp_match")
|
|
2559
|
+
return caseWhen([[matches, found]], null);
|
|
2560
|
+
if (!flags.includes("g"))
|
|
2561
|
+
return fn("unnest", [caseWhen([[matches, arrayOf([found])]], null)]);
|
|
2562
|
+
// each match's group as a one-element array: list_value of the unnested matches
|
|
2563
|
+
return fn("list_value", [fn("unnest", [fn("regexp_extract_all", [this.clone(s), this.clone(re), intConst(groups === 0 ? 0 : 1), this.clone(options)])])]);
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* A value's TEXT as Postgres writes it where DuckDB's cast to VARCHAR writes another — its output function's (what
|
|
2567
|
+
* `concat`, `format`, `||` and to_json write) or, `asCast`, an explicit cast's (which differs from the output for a
|
|
2568
|
+
* boolean alone: `true::text` is `true`, its output `t`). Null where DuckDB's VARCHAR is already Postgres's text.
|
|
2569
|
+
*/
|
|
2570
|
+
pgText(n, type, asCast) {
|
|
2571
|
+
if (type === null)
|
|
2572
|
+
return null;
|
|
2573
|
+
// a char(n)'s output is padded; its cast to text drops the padding
|
|
2574
|
+
if (type === BPCHAR)
|
|
2575
|
+
return asCast ? fn("rtrim", [n]) : null;
|
|
2576
|
+
// a float cast to numeric is a DuckDB double; Postgres's numeric holds the float's shortest digits, written plainly
|
|
2577
|
+
// (`round(x)::numeric::text` is `25`, never `25.0`; `1e-7` is `0.0000001`)
|
|
2578
|
+
if (type === NUMERIC && kindOf(n) === "TypeCast" && (this.typed(n)?.typmod ?? -1) < 0) {
|
|
2579
|
+
const inner = bodyOf(n).arg, from = this.typeOf(inner);
|
|
2580
|
+
if (from !== null && FLOATS.has(from))
|
|
2581
|
+
return fn("__mason_float_numeric_text", [cast(inner, ["double"]), boolConst(from === FLOAT4)]);
|
|
2582
|
+
}
|
|
2583
|
+
// an inet's output drops a full mask (`10.3.6.45`, `::1`); its cast to text keeps it (`10.3.6.45/32`), as stored
|
|
2584
|
+
if (type === INET)
|
|
2585
|
+
return asCast ? null : inetOut(n);
|
|
2586
|
+
// a bytea's text is its hex (bytea_output = hex): `\x00ff`
|
|
2587
|
+
if (type === BYTEA)
|
|
2588
|
+
return concat(strConst("\\x"), fn("lower", [fn("hex", [n])]));
|
|
2589
|
+
if (FLOATS.has(type))
|
|
2590
|
+
return fn("__mason_float_text", [cast(n, ["double"]), boolConst(type === FLOAT4)]);
|
|
2591
|
+
if (type === BOOL)
|
|
2592
|
+
return asCast ? null : caseWhen([[n, strConst("t")], [not(this.clone(n)), strConst("f")]], null);
|
|
2593
|
+
if (type === INTERVAL)
|
|
2594
|
+
return fn("__mason_interval_text", [n]);
|
|
2595
|
+
if (ARRAYS.has(type))
|
|
2596
|
+
return fn("__mason_array_text", [arrayCast(n, "varchar"), boolConst(type === 1000)]);
|
|
2597
|
+
if (type === JSONB)
|
|
2598
|
+
return fn("__mason_jsonb_text", [n]);
|
|
2599
|
+
// a year before 1 is DuckDB's `4713-11-24 (BC)`, Postgres's `4713-11-24 BC` (after the time, for a timestamp)
|
|
2600
|
+
if (type === DATE || type === TIMESTAMP || type === TIMESTAMPTZ)
|
|
2601
|
+
return fn("regexp_replace", [cast(n, ["varchar"]), strConst("^(\\S+) \\(BC\\)(.*)$"), strConst("\\1\\2 BC")]);
|
|
2602
|
+
return null;
|
|
2603
|
+
}
|
|
2604
|
+
/** a value as json_build_object / json_agg / to_json write it: its text, and how (by its Postgres type) */
|
|
2605
|
+
jsonValue(n, oid, fnName) {
|
|
2606
|
+
// an array: each element written as its own type's json (src/duck/functions.ts jsonArray), from its text as DuckDB
|
|
2607
|
+
// writes it — only for elements whose text is Postgres's (not a float's NaN, a timestamp's T, an interval's or a
|
|
2608
|
+
// char(n)'s). Never DuckDB's to_json of the list: it quotes a jsonb element's text as a string (`["\"x\""]`)
|
|
2609
|
+
const element = oid === null ? undefined : JSON_ARRAY_ELEMENT[oid];
|
|
2610
|
+
if (element !== undefined)
|
|
2611
|
+
return { kind: "r", value: fn("__mason_json_array", [strConst(element), arrayCast(n, "varchar")]) };
|
|
2612
|
+
if (oid === JSONB)
|
|
2613
|
+
return { kind: "k", value: cast(n, ["varchar"]) };
|
|
2614
|
+
if (oid === TIMESTAMPTZ)
|
|
2615
|
+
return { kind: "z", value: this.pgText(n, oid, false) ?? cast(n, ["varchar"]) };
|
|
2616
|
+
if (oid === TIMESTAMP)
|
|
2617
|
+
return { kind: "t", value: this.pgText(n, oid, false) ?? cast(n, ["varchar"]) };
|
|
2618
|
+
if (oid !== null && (FLOATS.has(oid) || oid === NUMERIC))
|
|
2619
|
+
return { kind: "n", value: this.pgText(n, oid, false) ?? cast(n, ["varchar"]) };
|
|
2620
|
+
if (oid !== null && (INTEGERS.has(oid) || oid === BOOL || oid === JSON_OID))
|
|
2621
|
+
return { kind: "r", value: cast(n, ["varchar"]) };
|
|
2622
|
+
if (oid !== null && (TEXTS.has(oid) || oid === UUID || oid === DATE || oid === TIME || oid === INTERVAL || oid === INET))
|
|
2623
|
+
return { kind: "s", value: this.pgText(n, oid, false) ?? cast(n, ["varchar"]) };
|
|
2624
|
+
throw new Unrewritable(`${fnName}() of a value of type OID ${oid ?? "unknown"} is not supported by Mason`, "cast the value to text, a number, a boolean, a timestamp or json first", "json-of-type");
|
|
2625
|
+
}
|
|
2626
|
+
/** EXTRACT, as the exact numeric Postgres returns: `epoch` and `second` with their fraction, the rest whole */
|
|
2627
|
+
extract(args) {
|
|
2628
|
+
const field = (constString(args[0]) ?? "").toLowerCase();
|
|
2629
|
+
const x = args[1];
|
|
2630
|
+
const type = this.typeOf(x);
|
|
2631
|
+
this.unitSupported(field, type);
|
|
2632
|
+
// an interval's parts as Postgres computes them (src/duck/functions.ts intervalPart)
|
|
2633
|
+
if (type === INTERVAL)
|
|
2634
|
+
return cast(fn("__mason_interval_part", [strConst(field), x]), ["decimal", "38", INTERVAL_SCALE[field] ?? "0"]);
|
|
2635
|
+
// a field that grows with time is ±Infinity of an infinite time, which a DECIMAL cannot hold: refused when met
|
|
2636
|
+
if ((type === DATE || type === TIMESTAMP || type === TIMESTAMPTZ) && MONOTONIC.has(field)) {
|
|
2637
|
+
const inner = this.extractFinite(field, this.clone(x), type) ?? fn("date_part", [strConst(field), this.clone(x)]);
|
|
2638
|
+
// tested as NOT isfinite: isfinite of a NULL is NULL, which would take the infinite branch
|
|
2639
|
+
return caseWhen([[not(fn("isfinite", [x])), fn("error", [strConst(`mason: extract(${field} …) of an infinite date or timestamp is not supported by Mason (Mason gap infinite-extract)`)])]], inner);
|
|
2640
|
+
}
|
|
2641
|
+
return this.extractFinite(field, x, type);
|
|
2642
|
+
}
|
|
2643
|
+
/** Postgres refuses a unit a type does not have (at run time, so its analysis does not): its words, its SQLSTATE */
|
|
2644
|
+
unitSupported(field, type) {
|
|
2645
|
+
const name = type === DATE ? "date" : type === TIMESTAMP ? "timestamp without time zone" : type === TIME ? "time without time zone" : null;
|
|
2646
|
+
const refused = type === DATE ? ["hour", "minute", "second", "milliseconds", "microseconds", "timezone", "timezone_hour", "timezone_minute", "timezone_m"]
|
|
2647
|
+
: type === TIMESTAMP || type === TIME ? ["timezone", "timezone_hour", "timezone_minute"] : [];
|
|
2648
|
+
if (name !== null && refused.includes(field))
|
|
2649
|
+
throw new Unrewritable(`unit "${field}" not supported for type ${name}`, "", { postgres: "0A000" });
|
|
2650
|
+
}
|
|
2651
|
+
extractFinite(field, x, type) {
|
|
2652
|
+
const micros = (expr, scale) => op("*", cast(expr, ["decimal", "38", "0"]), numConst(scale));
|
|
2653
|
+
switch (field) {
|
|
2654
|
+
case "epoch":
|
|
2655
|
+
return type === DATE ? cast(fn("epoch", [x]), ["decimal", "38", "0"]) : micros(fn("epoch_us", [x]), "0.000001");
|
|
2656
|
+
case "second": return micros(fn("microsecond", [x]), "0.000001");
|
|
2657
|
+
case "milliseconds": return micros(fn("microsecond", [x]), "0.001");
|
|
2658
|
+
case "microseconds": return cast(fn("microsecond", [x]), ["decimal", "38", "0"]);
|
|
2659
|
+
case "julian": throw new Unrewritable("extract(julian …) is not supported by Mason", "compute it from extract(epoch …) / 86400 + 2440587.5", "extract-julian");
|
|
2660
|
+
default: return null;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
/** date_part: Postgres's double precision, a second with its fraction; ±Infinity of an infinite time */
|
|
2664
|
+
datePart(args) {
|
|
2665
|
+
const field = (constString(args[0]) ?? "").toLowerCase();
|
|
2666
|
+
const x = args[1];
|
|
2667
|
+
const type = this.typeOf(x);
|
|
2668
|
+
this.unitSupported(field, type);
|
|
2669
|
+
if (type === INTERVAL)
|
|
2670
|
+
return cast(fn("__mason_interval_part", [strConst(field), x]), ["double"]);
|
|
2671
|
+
if ((type === DATE || type === TIMESTAMP || type === TIMESTAMPTZ) && MONOTONIC.has(field)) {
|
|
2672
|
+
const inner = this.datePartFinite(field, this.clone(x)) ?? fn("date_part", [strConst(field), this.clone(x)]);
|
|
2673
|
+
// tested as NOT isfinite: isfinite of a NULL is NULL, which would take an infinite branch
|
|
2674
|
+
const infinite = () => not(fn("isfinite", [this.clone(x)]));
|
|
2675
|
+
return caseWhen([[and([infinite(), op(">", this.clone(x), cast(strConst("2000-01-01"), ["date"]))]), cast(strConst("Infinity"), ["double"])],
|
|
2676
|
+
[infinite(), cast(strConst("-Infinity"), ["double"])]], inner);
|
|
2677
|
+
}
|
|
2678
|
+
return this.datePartFinite(field, x);
|
|
2679
|
+
}
|
|
2680
|
+
datePartFinite(field, x) {
|
|
2681
|
+
switch (field) {
|
|
2682
|
+
case "second": return op("/", cast(fn("microsecond", [x]), ["double"]), numConst("1000000.0"));
|
|
2683
|
+
case "milliseconds": return op("/", cast(fn("microsecond", [x]), ["double"]), numConst("1000.0"));
|
|
2684
|
+
case "epoch": return this.typeOf(x) === DATE ? cast(fn("epoch", [x]), ["double"]) : op("/", cast(fn("epoch_us", [x]), ["double"]), numConst("1000000.0"));
|
|
2685
|
+
default: return null;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
// --------------------------------------------------------------------------------------------- casts, constants
|
|
2689
|
+
/** a timestamp (or timestamptz) rounded to `p` decimals of a second: half away from 2000-01-01, an infinity kept */
|
|
2690
|
+
roundTime(x, p) {
|
|
2691
|
+
const unit = intConst(10 ** (6 - p));
|
|
2692
|
+
const e = () => op("-", fn("epoch_us", [this.cp(x)]), numConst("946684800000000"));
|
|
2693
|
+
const rounded = op("*", op("*", fn("sign", [e()]), op("//", op("+", fn("abs", [e()]), intConst(10 ** (6 - p) / 2)), unit)), this.cp(unit));
|
|
2694
|
+
return caseWhen([[fn("isfinite", [this.cp(x)]), op("+", this.cp(x), fn("to_microseconds", [op("-", rounded, e())]))]], x);
|
|
2695
|
+
}
|
|
2696
|
+
/** a text read as `type` (with `typmod`) by Postgres's own input function, a chunk at a time (`__mason_pg_*`,
|
|
2697
|
+
* src/duck/functions.ts), under the zone DuckDB's session has; null for a type DuckDB reads as Postgres does */
|
|
2698
|
+
pgInput(arg, type, typmod) {
|
|
2699
|
+
const p = typmod >= 4 ? (typmod - 4) >> 16 : -1, sc = typmod >= 4 ? (typmod - 4) & 0xffff : -1;
|
|
2700
|
+
const spelled = {
|
|
2701
|
+
[DATE]: ["date", "date", null], [TIMESTAMP]: [typmod >= 0 ? `timestamp(${typmod})` : "timestamp", "timestamp", null],
|
|
2702
|
+
[TIMESTAMPTZ]: [typmod >= 0 ? `timestamptz(${typmod})` : "timestamptz", "timestamptz", null], [INTERVAL]: ["interval", "interval", null],
|
|
2703
|
+
[BOOL]: ["boolean", "bool", null], [UUID]: ["uuid", "text", ["uuid"]], [INT2]: ["smallint", "int8", ["smallint"]], [INT4]: ["integer", "int8", ["integer"]], [INT8]: ["bigint", "int8", null],
|
|
2704
|
+
// a numeric is its digits beside it (exactOf); here the value DuckDB computes with
|
|
2705
|
+
[NUMERIC]: [p >= 0 ? `numeric(${p},${sc})` : "numeric", "text", p >= 0 && p <= 38 ? ["decimal", String(p), String(sc)] : ["double"]],
|
|
2706
|
+
};
|
|
2707
|
+
const s = spelled[type];
|
|
2708
|
+
if (s === undefined)
|
|
2709
|
+
return null;
|
|
2710
|
+
const call = pgCall(s[1], `a1::${s[0]}`, [arg]);
|
|
2711
|
+
return s[2] === null ? call : cast(call, s[2]);
|
|
2712
|
+
}
|
|
2713
|
+
typeCast(b) {
|
|
2714
|
+
const target = lastName(b.typeName?.names);
|
|
2715
|
+
const t = this.typed({ TypeCast: b });
|
|
2716
|
+
const arg = b.arg;
|
|
2717
|
+
// a GROUP BY or ORDER BY repeating `INTERVAL '1 day'` of the SELECT list: its literal has no analysed node of its
|
|
2718
|
+
// own (Postgres refers to the list's), and is the value the list's was parsed to — the same SQL on both sides
|
|
2719
|
+
if (t?.node === "CONST" && kindOf(arg) === "A_Const" && this.at(bodyOf(arg), ["CONST"]) === null)
|
|
2720
|
+
this.constLike.set(bodyOf(arg), t);
|
|
2721
|
+
const from = this.typeOf(arg);
|
|
2722
|
+
// to an enum (or an array of one): a label, a text read by Postgres's rules, or the enum it already is
|
|
2723
|
+
if (t !== null && (this.enums.labels.has(t.type) || this.enums.arrays.has(t.type))) {
|
|
2724
|
+
if (from === t.type)
|
|
2725
|
+
return arg;
|
|
2726
|
+
if (constString(arg) !== null)
|
|
2727
|
+
return this.enumValue(arg, t.type, constString(arg));
|
|
2728
|
+
if (from !== null && TEXTS.has(from))
|
|
2729
|
+
return this.enumValue(arg, t.type, null);
|
|
2730
|
+
}
|
|
2731
|
+
const isArray = Array.isArray(b.typeName?.arrayBounds) && b.typeName.arrayBounds.length > 0;
|
|
2732
|
+
if (isArray) {
|
|
2733
|
+
// an array literal ('{a,b}'::text[]): its elements, read as array_in reads them
|
|
2734
|
+
const literal = constString(arg);
|
|
2735
|
+
const element = ARRAY_ELEMENT[target];
|
|
2736
|
+
if (literal === null || element === undefined)
|
|
2737
|
+
return null;
|
|
2738
|
+
const list = arrayOf(arrayLiteral(literal).map((e) => e === null ? nullConst() : strConst(e)));
|
|
2739
|
+
return element === "varchar" ? list : arrayCast(list, element);
|
|
2740
|
+
}
|
|
2741
|
+
// a text read as a date, a time, an interval, a boolean, an integer or a numeric: by Postgres's own input function
|
|
2742
|
+
// (src/duck/pg-eval.ts), which reads `01/02/2026`, `1 day ago`, ` yes `, `0x1F` and `1_000` (DuckDB reads ISO alone)
|
|
2743
|
+
if (t !== null && from !== null && TEXTS.has(from) && from !== UNKNOWN && constString(arg) === null) {
|
|
2744
|
+
const read = this.pgInput(arg, t.type, t.typmod);
|
|
2745
|
+
if (read !== null) {
|
|
2746
|
+
this.typedLike.set(bodyOf(read), { TypeCast: b });
|
|
2747
|
+
if (target === "interval" && Array.isArray(b.typeName?.typmods) && b.typeName.typmods.length > 0)
|
|
2748
|
+
return intervalFields(read, constInt(b.typeName.typmods[0]));
|
|
2749
|
+
return read;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
// a value's text is Postgres's (an array's `{a,b}`, a jsonb's canonical text, a float's shortest digits …), cut to a
|
|
2753
|
+
// varchar(n)'s length as the explicit cast cuts it
|
|
2754
|
+
// to char(n): the text cut and padded to n
|
|
2755
|
+
if (t !== null && t.type === BPCHAR && t.typmod > 4) {
|
|
2756
|
+
const text = from === BPCHAR ? arg : this.pgText(arg, from, true) ?? cast(arg, ["varchar"]);
|
|
2757
|
+
return fn("rpad", [fn("left", [text, intConst(t.typmod - 4)]), intConst(t.typmod - 4), strConst(" ")]);
|
|
2758
|
+
}
|
|
2759
|
+
if (t !== null && (t.type === TEXT || t.type === VARCHAR)) {
|
|
2760
|
+
const text = this.pgText(arg, from, true);
|
|
2761
|
+
if (text !== null)
|
|
2762
|
+
return t.type === VARCHAR && t.typmod > 4 ? fn("left", [text, intConst(t.typmod - 4)]) : text;
|
|
2763
|
+
}
|
|
2764
|
+
// timestamp(p) / timestamptz(p) of a time: rounded to p decimals of a second as AdjustTimestampForTypmod rounds, half
|
|
2765
|
+
// away from Postgres's epoch (2000-01-01: `1960-01-01 00:00:00.5` is `00:00:00`); DuckDB rounds otherwise or not at all
|
|
2766
|
+
if (t !== null && (t.type === TIMESTAMP || t.type === TIMESTAMPTZ) && kindOf(arg) !== "A_Const" && from !== null && (from === DATE || from === TIMESTAMP || from === TIMESTAMPTZ)) {
|
|
2767
|
+
const own = from === t.type ? arg : cast(arg, [t.type === TIMESTAMPTZ ? "timestamptz" : "timestamp"]);
|
|
2768
|
+
return t.typmod >= 0 && t.typmod < 6 ? this.roundTime(own, t.typmod) : own;
|
|
2769
|
+
}
|
|
2770
|
+
if (target === "name")
|
|
2771
|
+
return fn("left", [this.pgText(arg, from, true) ?? cast(arg, ["varchar"]), intConst(63)]);
|
|
2772
|
+
if (target === "time" && from === TIMESTAMPTZ)
|
|
2773
|
+
return cast(cast(arg, ["timestamp"]), ["time"]);
|
|
2774
|
+
if (target === "interval" && Array.isArray(b.typeName?.typmods) && b.typeName.typmods.length > 0)
|
|
2775
|
+
return intervalFields(arg, constInt(b.typeName.typmods[0]));
|
|
2776
|
+
// a text or a json read as jsonb is jsonb's canonical text (keys sorted and once, `1.0` kept, whitespace Postgres's),
|
|
2777
|
+
// as the copy stores a jsonb column
|
|
2778
|
+
if (target === "jsonb" && from !== JSONB && from !== null && (TEXTS.has(from) || from === JSON_OID))
|
|
2779
|
+
return cast(fn("__mason_jsonb_text", [cast(arg, ["varchar"])]), ["json"]);
|
|
2780
|
+
if (target === "jsonb" || target === "json") {
|
|
2781
|
+
b.typeName = { names: [S("json")], typemod: -1 };
|
|
2782
|
+
return null;
|
|
2783
|
+
}
|
|
2784
|
+
if (target === "numeric" || target === "decimal") {
|
|
2785
|
+
const typmods = (b.typeName?.typmods ?? []);
|
|
2786
|
+
if (typmods.length > 0) {
|
|
2787
|
+
const p = constInt(typmods[0]), s = typmods.length > 1 ? constInt(typmods[1]) : 0;
|
|
2788
|
+
if (p !== null && p > 38)
|
|
2789
|
+
return cast(arg, ["double"]);
|
|
2790
|
+
if (p !== null)
|
|
2791
|
+
return cast(arg, ["decimal", String(p), String(s ?? 0)]);
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2794
|
+
// an unconstrained numeric keeps the value's digits: an integer is a whole decimal, a literal text is read as a
|
|
2795
|
+
// decimal literal (BEFORE a numeric: Postgres types `'1.5'` as the cast's numeric, and as it stood DuckDB answered
|
|
2796
|
+
// the text, `2.5e2` for 250), a numeric stays what it is
|
|
2797
|
+
if (from !== null && INTEGERS.has(from))
|
|
2798
|
+
return cast(arg, ["decimal", "38", "0"]);
|
|
2799
|
+
// as numeric_in reads it (`1_000`, `0x1F`, `1.5e3`: one reader, src/lib/pg-numeric.ts); NaN and Infinity are no
|
|
2800
|
+
// DuckDB decimal and keep the double below
|
|
2801
|
+
const s = constString(arg);
|
|
2802
|
+
const literal = s === null ? null : numericLiteral(s);
|
|
2803
|
+
if (literal !== null)
|
|
2804
|
+
return decimalConst(literal);
|
|
2805
|
+
if (from === NUMERIC)
|
|
2806
|
+
return arg;
|
|
2807
|
+
return cast(arg, ["double"]);
|
|
2808
|
+
}
|
|
2809
|
+
if ((target === "varchar" || target === "bpchar") && t !== null && t.typmod > 4 && target === "varchar") {
|
|
2810
|
+
// varchar(n) cuts the value to n characters, as Postgres's explicit cast does
|
|
2811
|
+
return fn("left", [cast(arg, ["varchar"]), intConst(t.typmod - 4)]);
|
|
2812
|
+
}
|
|
2813
|
+
return null;
|
|
2814
|
+
}
|
|
2815
|
+
aConst(b) {
|
|
2816
|
+
// a string Postgres read as an array ('{1}' beside an int[]): its elements, as array_in reads them
|
|
2817
|
+
if (b.sval !== undefined) {
|
|
2818
|
+
const t = this.at(b, ["CONST"]) ?? this.constLike.get(b) ?? null;
|
|
2819
|
+
// a label of an enum (or an array of them) Postgres read it as: DuckDB's ENUM, which compares by the declared order
|
|
2820
|
+
// where the text would compare as text
|
|
2821
|
+
const asEnum = t === null ? null : this.enumValue(strConst(String(b.sval.sval ?? "")), t.type, String(b.sval.sval ?? ""));
|
|
2822
|
+
if (asEnum !== null)
|
|
2823
|
+
return asEnum;
|
|
2824
|
+
// a date, timestamp or interval: the value Postgres parsed, never the text again — DuckDB reads only ISO, and not
|
|
2825
|
+
// `'01/02/2026'`, `'March 1, 2026'`, `'… BC'`, `'epoch'`, `'1 day ago'` or `'P1D'`
|
|
2826
|
+
// an inet literal as Postgres canonicalises it (`'10.3.6.45'` is `10.3.6.45/32`, as the copy stores the column)
|
|
2827
|
+
// a bytea literal (`'\x00ff'`, an escaped `'a\000b'`): the bytes Postgres read, by their hex
|
|
2828
|
+
if (t !== null && t.type === BYTEA && t.datum !== null && t.datum.length >= 4)
|
|
2829
|
+
return fn("unhex", [strConst(t.datum.slice(4).map((x) => x.toString(16).padStart(2, "0")).join(""))]);
|
|
2830
|
+
if (t !== null && t.type === INET && t.datum !== null) {
|
|
2831
|
+
const text = datumText(INET, t.datum);
|
|
2832
|
+
if (text !== null)
|
|
2833
|
+
return strConst(text);
|
|
2834
|
+
}
|
|
2835
|
+
if (t !== null && t.type === INTERVAL && t.datum !== null) {
|
|
2836
|
+
const parts = intervalDatum(t.datum);
|
|
2837
|
+
if (parts !== null)
|
|
2838
|
+
return cast(strConst(`${parts.months} months ${parts.days} days ${parts.micros} microseconds`), ["interval"]);
|
|
2839
|
+
}
|
|
2840
|
+
if (t !== null && (t.type === DATE || t.type === TIMESTAMP || t.type === TIMESTAMPTZ)) {
|
|
2841
|
+
const text = String(b.sval.sval ?? "");
|
|
2842
|
+
const word = /^\s*(now|today|tomorrow|yesterday)\s*$/i.exec(text)?.[1]?.toLowerCase();
|
|
2843
|
+
if (word !== undefined)
|
|
2844
|
+
return this.relativeTime(word, t.type);
|
|
2845
|
+
if (/\b(now|today|tomorrow|yesterday)\b/i.test(text)) {
|
|
2846
|
+
throw new Unrewritable(`the literal '${text}' is not supported by Mason: it means another time on each run`, "write current_date, now(), current_date + 1 or current_date - 1", "relative-time-literal");
|
|
2847
|
+
}
|
|
2848
|
+
const iso = t.datum === null ? null : datumText(t.type, t.datum);
|
|
2849
|
+
if (iso !== null)
|
|
2850
|
+
return cast(strConst(iso), [t.type === DATE ? "date" : t.type === TIMESTAMP ? "timestamp" : "timestamptz"]);
|
|
2851
|
+
}
|
|
2852
|
+
const element = t === null ? undefined : ARRAY_DUCK[t.type];
|
|
2853
|
+
if (element !== undefined) {
|
|
2854
|
+
const list = arrayOf(arrayLiteral(String(b.sval.sval ?? "")).map((e) => e === null ? nullConst() : strConst(e)));
|
|
2855
|
+
return element === "varchar" ? list : arrayCast(list, element);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
// a NULL of an enum type (`null::mood`): DuckDB's ENUM, which enum_first and enum_range need
|
|
2859
|
+
if (b.isnull === true) {
|
|
2860
|
+
const t = this.at(b, ["CONST"]) ?? this.constLike.get(b) ?? null;
|
|
2861
|
+
const labels = t === null ? undefined : this.enums.labels.get(t.type);
|
|
2862
|
+
if (labels !== undefined)
|
|
2863
|
+
return enumCast(nullConst(), labels);
|
|
2864
|
+
}
|
|
2865
|
+
// a numeric literal with an exponent is a positional decimal (DuckDB reads `1e-7` as a double)
|
|
2866
|
+
if (b.fval !== undefined && /[eE]/.test(String(b.fval.fval ?? ""))) {
|
|
2867
|
+
const t = this.at(b, ["CONST"]);
|
|
2868
|
+
if (t === null || t.type === NUMERIC)
|
|
2869
|
+
return decimalConst(positional(String(b.fval.fval)));
|
|
2870
|
+
}
|
|
2871
|
+
return null;
|
|
2872
|
+
}
|
|
2873
|
+
/** a value of an enum type (or an enum array type), as DuckDB's ENUM: a constant label, or one read by Postgres's rules
|
|
2874
|
+
* from a text (`__mason_enum_in`: 22P02 for a label the type lacks); null for any other type */
|
|
2875
|
+
enumValue(text, type, literal) {
|
|
2876
|
+
const own = this.enums.labels.get(type);
|
|
2877
|
+
if (own !== undefined)
|
|
2878
|
+
return enumCast(literal !== null ? strConst(literal) : fn("__mason_enum_in", [cast(text, ["varchar"]), strConst(this.userTypes.get(type) ?? "enum"), arrayOf(own.map(strConst))]), own);
|
|
2879
|
+
const element = this.enums.arrays.get(type);
|
|
2880
|
+
const labels = element === undefined ? undefined : this.enums.labels.get(element);
|
|
2881
|
+
if (labels === undefined)
|
|
2882
|
+
return null;
|
|
2883
|
+
if (literal === null)
|
|
2884
|
+
throw new Unrewritable("an array of an enum read from a text is not supported by Mason", "write the array as a literal, or ARRAY[…] of its labels", "enum-forms");
|
|
2885
|
+
const elements = arrayLiteral(literal);
|
|
2886
|
+
if (elements.some((e) => e !== null && !labels.includes(e)))
|
|
2887
|
+
return null;
|
|
2888
|
+
return enumCast(arrayOf(elements.map((e) => e === null ? nullConst() : strConst(e))), labels, true);
|
|
2889
|
+
}
|
|
2890
|
+
param(b) {
|
|
2891
|
+
// a parameter is bound as its text; Postgres decided its type, and DuckDB is told it
|
|
2892
|
+
const oid = this.params[Number(b.number) - 1];
|
|
2893
|
+
const asEnum = oid === undefined || !this.enums.labels.has(oid) ? null : this.enumValue({ ParamRef: b }, oid, null);
|
|
2894
|
+
if (asEnum !== null)
|
|
2895
|
+
return asEnum;
|
|
2896
|
+
const type = oid === undefined ? null : oid === NUMERIC ? ["double"] : duckTypeOf(oid);
|
|
2897
|
+
return type === null ? null : cast({ ParamRef: b }, type);
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* The reads a json read is made off (`(c -> 'a') -> 0`, `c #> '{a}'`), folded into one read's steps: the inner
|
|
2901
|
+
* read's text parsed again is the same tree (jsonb's canonical text is canonical; json's is the document's own
|
|
2902
|
+
* slice), so stepping on from it is stepping on in place — one parse instead of one per operator, and the native
|
|
2903
|
+
* read (jsonGet) sees the column. Reads of one operator family only (`->`'s steps are keys and indexes, `#>`'s
|
|
2904
|
+
* elements are either), with literal keys; null for anything else, which is read as written.
|
|
2905
|
+
*/
|
|
2906
|
+
jsonChain(node, either) {
|
|
2907
|
+
if (kindOf(node) !== "A_Expr")
|
|
2908
|
+
return null;
|
|
2909
|
+
const b = bodyOf(node);
|
|
2910
|
+
if (b.kind !== "AEXPR_OP" || lastName(b.name) !== (either ? "#>" : "->") || b.lexpr === undefined || b.rexpr === undefined)
|
|
2911
|
+
return null;
|
|
2912
|
+
const t = this.typed(node);
|
|
2913
|
+
const a0 = t?.args[0]?.type ?? null, a1 = t?.args[1]?.type ?? null;
|
|
2914
|
+
if (a0 === null || !JSONS.has(a0))
|
|
2915
|
+
return null;
|
|
2916
|
+
const r = b.rexpr;
|
|
2917
|
+
const own = constString(either ? jsonPath(r) : jsonSteps([r], a1 !== null && INTEGERS.has(a1)));
|
|
2918
|
+
if (own === null)
|
|
2919
|
+
return null;
|
|
2920
|
+
const deeper = this.jsonChain(b.lexpr, either);
|
|
2921
|
+
return { base: deeper === null ? b.lexpr : deeper.base, steps: [...(deeper === null ? [] : deeper.steps), ...JSON.parse(own)] };
|
|
2922
|
+
}
|
|
2923
|
+
// --------------------------------------------------------------------------------------------- FROM
|
|
2924
|
+
rangeFunction(b) {
|
|
2925
|
+
const items = (b.functions ?? []);
|
|
2926
|
+
const first = items[0] !== undefined ? bodyOf(items[0]).items?.[0] : undefined;
|
|
2927
|
+
if (first === undefined || kindOf(first) !== "FuncCall")
|
|
2928
|
+
return null;
|
|
2929
|
+
const call = bodyOf(first);
|
|
2930
|
+
const name = lastName(call.funcname);
|
|
2931
|
+
const args = (call.args ?? []);
|
|
2932
|
+
const alias = b.alias;
|
|
2933
|
+
const aliasName = alias?.aliasname;
|
|
2934
|
+
const colnames = names(alias?.colnames);
|
|
2935
|
+
for (const item of items)
|
|
2936
|
+
for (const f of (bodyOf(item).items ?? []))
|
|
2937
|
+
if (isNode(f) && kindOf(f) === "FuncCall")
|
|
2938
|
+
this.inFrom.add(bodyOf(f));
|
|
2939
|
+
const canonical = name.startsWith("jsonb");
|
|
2940
|
+
switch (name) {
|
|
2941
|
+
// jsonpath's rows, as Postgres finds them (src/duck/pg-eval.ts), unnested
|
|
2942
|
+
case "jsonb_path_query": {
|
|
2943
|
+
if (b.ordinality === true)
|
|
2944
|
+
throw new Unrewritable(`${name}() WITH ORDINALITY is not supported by Mason`, "number the rows with row_number() OVER () instead", "with-ordinality");
|
|
2945
|
+
if (args.length < 2 || args.length > 4 || args.some((a) => kindOf(a) === "NamedArgExpr"))
|
|
2946
|
+
return null;
|
|
2947
|
+
const list = pgCall("list", `ARRAY(SELECT mason_p::text FROM jsonb_path_query(${args.map((_, i) => `a${i + 1}::${JSONPATH_ARGS[i]}`).join(", ")}) AS mason_p)`, args);
|
|
2948
|
+
const column = colnames[0] ?? aliasName ?? name;
|
|
2949
|
+
const item = bodyOf(items[0]);
|
|
2950
|
+
return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [fn("unnest", [arrayCast(list, "json")]), ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
|
|
2951
|
+
}
|
|
2952
|
+
case "jsonb_array_elements":
|
|
2953
|
+
case "json_array_elements":
|
|
2954
|
+
case "jsonb_array_elements_text":
|
|
2955
|
+
case "json_array_elements_text": {
|
|
2956
|
+
if (b.ordinality === true)
|
|
2957
|
+
throw new Unrewritable(`${name}() WITH ORDINALITY is not supported by Mason`, "number the elements with row_number() OVER () instead", "with-ordinality");
|
|
2958
|
+
const text = name.endsWith("_text");
|
|
2959
|
+
const elements = fn("__mason_json_elements", [jsonArg(args[0]), boolConst(canonical), boolConst(text)]);
|
|
2960
|
+
const list = text ? elements : arrayCast(elements, "json");
|
|
2961
|
+
// a scalar function's column is named by its alias (or `value`), and so is DuckDB's unnest column here
|
|
2962
|
+
const column = colnames[0] ?? aliasName ?? "value";
|
|
2963
|
+
const unnest = fn("unnest", [list]);
|
|
2964
|
+
const item = bodyOf(items[0]);
|
|
2965
|
+
return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [unnest, ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
|
|
2966
|
+
}
|
|
2967
|
+
case "generate_series": {
|
|
2968
|
+
// a series of times as DuckDB's list, unnested: its table function of times answers no rows for EVERY outer row
|
|
2969
|
+
// once one row's series is empty, when correlated (a LATERAL or a subquery; DuckDB 2.0.0, threads or none),
|
|
2970
|
+
// where the list form and the series of integers are right
|
|
2971
|
+
const type = this.typed({ FuncCall: call })?.type ?? null;
|
|
2972
|
+
if (items.length !== 1 || (type !== TIMESTAMP && type !== TIMESTAMPTZ))
|
|
2973
|
+
return null;
|
|
2974
|
+
if (b.ordinality === true)
|
|
2975
|
+
throw new Unrewritable(`${name}() of times WITH ORDINALITY is not supported by Mason`, "number the rows with row_number() OVER (ORDER BY the value) instead", "with-ordinality");
|
|
2976
|
+
const column = colnames[0] ?? aliasName ?? name;
|
|
2977
|
+
const item = bodyOf(items[0]);
|
|
2978
|
+
return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [fn("unnest", [first]), ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
|
|
2979
|
+
}
|
|
2980
|
+
case "jsonb_to_record":
|
|
2981
|
+
case "json_to_record": {
|
|
2982
|
+
// each column as Postgres reads it from the object (src/duck/pg-eval.ts, a chunk at a time), in a lateral row
|
|
2983
|
+
const defs = (b.coldeflist ?? []).map((d) => bodyOf(d));
|
|
2984
|
+
if (defs.length === 0 || args.length !== 1 || b.ordinality === true)
|
|
2985
|
+
return null;
|
|
2986
|
+
const spelled = defs.map((d) => ({ name: String(d.colname), ...recordColumn(d.typeName) }));
|
|
2987
|
+
const bad = spelled.find((c) => c.read === null);
|
|
2988
|
+
if (bad !== undefined)
|
|
2989
|
+
throw new Unrewritable(`${name}() with a column of type ${bad.spelled} is not supported by Mason`, "read the column as text and cast it outside: AS x(c text)", "unverified");
|
|
2990
|
+
const list = spelled.map((c) => `"${c.name.replaceAll('"', '""')}" ${c.spelled}`).join(", ");
|
|
2991
|
+
const sub = {
|
|
2992
|
+
targetList: spelled.map((c) => {
|
|
2993
|
+
const [kind, duck] = c.read;
|
|
2994
|
+
const call = pgCall(kind, `(SELECT mason_r."${c.name.replaceAll('"', '""')}" FROM ${name}(a1::${canonical ? "jsonb" : "json"}) AS mason_r(${list}))`, [this.clone(args[0])]);
|
|
2995
|
+
return { ResTarget: { name: c.name, val: duck === null ? call : cast(call, duck) } };
|
|
2996
|
+
}),
|
|
2997
|
+
op: "SETOP_NONE",
|
|
2998
|
+
};
|
|
2999
|
+
this.done.add(sub);
|
|
3000
|
+
return { RangeSubselect: { lateral: true, subquery: { SelectStmt: sub }, alias: { aliasname: aliasName ?? name } } };
|
|
3001
|
+
}
|
|
3002
|
+
case "jsonb_each":
|
|
3003
|
+
case "json_each":
|
|
3004
|
+
case "jsonb_each_text":
|
|
3005
|
+
case "json_each_text": {
|
|
3006
|
+
if (b.ordinality === true)
|
|
3007
|
+
throw new Unrewritable(`${name}() WITH ORDINALITY is not supported by Mason`, "number the members with row_number() OVER () instead", "with-ordinality");
|
|
3008
|
+
const text = name.endsWith("_text");
|
|
3009
|
+
// each member's key and value, two lists of one length that unnest side by side
|
|
3010
|
+
const sub = {
|
|
3011
|
+
targetList: [
|
|
3012
|
+
{ ResTarget: { name: colnames[0] ?? "key", val: fn("unnest", [fn("__mason_json_keys", [jsonArg(args[0]), boolConst(canonical)])]) } },
|
|
3013
|
+
{ ResTarget: { name: colnames[1] ?? "value", val: fn("unnest", [fn("__mason_json_values", [this.clone(args[0]), boolConst(canonical), boolConst(text)])]) } },
|
|
3014
|
+
],
|
|
3015
|
+
op: "SETOP_NONE",
|
|
3016
|
+
};
|
|
3017
|
+
this.done.add(sub);
|
|
3018
|
+
return { RangeSubselect: { lateral: true, subquery: { SelectStmt: sub }, alias: { aliasname: aliasName ?? name } } };
|
|
3019
|
+
}
|
|
3020
|
+
case "jsonb_object_keys":
|
|
3021
|
+
case "json_object_keys": {
|
|
3022
|
+
const column = colnames[0] ?? aliasName ?? name;
|
|
3023
|
+
const unnest = fn("unnest", [fn("__mason_json_keys", [args[0], boolConst(canonical)])]);
|
|
3024
|
+
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)] } } };
|
|
3026
|
+
}
|
|
3027
|
+
default: return null;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
/** a cell an exact program cannot take (in a grouped SELECT, a column not grouped): the column stays DuckDB's */
|
|
3032
|
+
const NOT_EXACT = new Error("not exact");
|
|
3033
|
+
/** a SELECT list item that is `*` or `t.*` */
|
|
3034
|
+
/** a set operation's arm (libpg-query's `larg` / `rarg`: a SelectStmt's body, unwrapped), as the node it is */
|
|
3035
|
+
const armOf = (key, v) => (key === "larg" || key === "rarg") && v !== null && typeof v === "object" && !Array.isArray(v) && !isNode(v) ? { SelectStmt: v } : null;
|
|
3036
|
+
const hasStar = (v) => isNode(v) && kindOf(v) === "ColumnRef" && bodyOf(v).fields.some((f) => kindOf(f) === "A_Star");
|
|
3037
|
+
/** whether a program does what DuckDB would compute otherwise: a division, a rounding, a scale chosen per value; a
|
|
3038
|
+
* sum, a difference or a product of DuckDB's decimals is DuckDB's own right answer */
|
|
3039
|
+
function worthComputing(p) {
|
|
3040
|
+
switch (p.op) {
|
|
3041
|
+
// a parameter's cell is its text: DuckDB computes with it as a double
|
|
3042
|
+
case "cell": return p.key.startsWith("$");
|
|
3043
|
+
case "literal": return false;
|
|
3044
|
+
// DuckDB's decimals may be fewer than Postgres's (src/duck/sync.ts copies a numeric(65,30) at its values' scale)
|
|
3045
|
+
case "pad": return true;
|
|
3046
|
+
case "add":
|
|
3047
|
+
case "sub":
|
|
3048
|
+
case "mul": return worthComputing(p.args[0]) || worthComputing(p.args[1]);
|
|
3049
|
+
case "neg":
|
|
3050
|
+
case "abs": return worthComputing(p.arg);
|
|
3051
|
+
default: return true;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
/** the program with each cell's key moved by `by` (the cells of the columns before it) */
|
|
3055
|
+
function offsetCells(p, by) {
|
|
3056
|
+
switch (p.op) {
|
|
3057
|
+
case "cell": return p.key.startsWith("$") ? p : { op: "cell", key: String(Number(p.key) + by) };
|
|
3058
|
+
case "literal": return p;
|
|
3059
|
+
case "add":
|
|
3060
|
+
case "sub":
|
|
3061
|
+
case "mul":
|
|
3062
|
+
case "div":
|
|
3063
|
+
case "mod": return { op: p.op, args: [offsetCells(p.args[0], by), offsetCells(p.args[1], by)] };
|
|
3064
|
+
case "neg":
|
|
3065
|
+
case "abs":
|
|
3066
|
+
case "floor":
|
|
3067
|
+
case "ceil": return { op: p.op, arg: offsetCells(p.arg, by) };
|
|
3068
|
+
case "round":
|
|
3069
|
+
case "trunc":
|
|
3070
|
+
case "pad": return { op: p.op, arg: offsetCells(p.arg, by), scale: p.scale };
|
|
3071
|
+
case "typmod": return { op: "typmod", arg: offsetCells(p.arg, by), precision: p.precision, scale: p.scale };
|
|
3072
|
+
case "greatest":
|
|
3073
|
+
case "least":
|
|
3074
|
+
case "coalesce": return { op: p.op, args: p.args.map((x) => offsetCells(x, by)) };
|
|
3075
|
+
case "stddev": return { ...p, args: [offsetCells(p.args[0], by), offsetCells(p.args[1], by), offsetCells(p.args[2], by)] };
|
|
3076
|
+
case "pick": return { op: "pick", index: offsetCells(p.index, by), args: p.args.map((x) => offsetCells(x, by)) };
|
|
3077
|
+
case "rescale": return { op: "rescale", arg: offsetCells(p.arg, by), scale: offsetCells(p.scale, by) };
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
/** an expression's shape: its tree without offsets */
|
|
3081
|
+
const shapeOf = (n) => JSON.stringify(n, (k, v) => k === "location" ? undefined : v);
|
|
3082
|
+
const strip = (n) => kindOf(n) === "CollateClause" ? bodyOf(n).arg : n;
|
|
3083
|
+
/** glibc's order, as the source was MEASURED to order text (src/duck/libc-order.ts: its levels as one text, ICU's
|
|
3084
|
+
* accent and case levels over the characters of the first), then the bytes, as Postgres breaks a tie */
|
|
3085
|
+
function libcKeys(copy, e, icu) {
|
|
3086
|
+
return [fn("__mason_libc_key", [copy(e), fn("icu_sort_key", [fn("__mason_libc_kept", [copy(e)]), strConst(icu)])]), e];
|
|
3087
|
+
}
|
|
3088
|
+
/** the libc keys as ONE text that orders as they do: the key, a space (below every hex digit, so a shorter key
|
|
3089
|
+
* orders first), the value's own bytes in hex */
|
|
3090
|
+
function libcKey(copy, e, icu) {
|
|
3091
|
+
const [key, bytes] = libcKeys(copy, e, icu);
|
|
3092
|
+
return concat(key, strConst(" "), fn("hex", [bytes]));
|
|
3093
|
+
}
|
|
3094
|
+
/** Postgres's array types whose elements are DuckDB's of these names */
|
|
3095
|
+
/**
|
|
3096
|
+
* A date's or timestamp's datum (src/duck/pgtree.ts `datumBytes`) as ISO text DuckDB reads back to the same value: a
|
|
3097
|
+
* date is days since 2000-01-01 (int32), a timestamp microseconds (int64), little-endian; a year before 1 is
|
|
3098
|
+
* astronomical (`-0043` is 44 BC), a timestamptz's in UTC (`+00`). Null where the bytes are not one.
|
|
3099
|
+
*/
|
|
3100
|
+
/** an IPv6 address as Postgres writes it (inet_net_ntop): the longest run of two or more zero words as `::` (the first
|
|
3101
|
+
* such run), hex words without leading zeros, and an IPv4-compatible or -mapped address's tail in dotted form */
|
|
3102
|
+
export function ipv6Text(addr) {
|
|
3103
|
+
const words = Array.from({ length: 8 }, (_, i) => (addr[2 * i] << 8) | addr[2 * i + 1]);
|
|
3104
|
+
let best = { base: -1, len: 0 }, cur = { base: -1, len: 0 };
|
|
3105
|
+
words.forEach((w, i) => {
|
|
3106
|
+
if (w === 0) {
|
|
3107
|
+
if (cur.base < 0)
|
|
3108
|
+
cur = { base: i, len: 1 };
|
|
3109
|
+
else
|
|
3110
|
+
cur.len++;
|
|
3111
|
+
}
|
|
3112
|
+
else if (cur.base >= 0) {
|
|
3113
|
+
if (best.base < 0 || cur.len > best.len)
|
|
3114
|
+
best = cur;
|
|
3115
|
+
cur = { base: -1, len: 0 };
|
|
3116
|
+
}
|
|
3117
|
+
});
|
|
3118
|
+
if (cur.base >= 0 && (best.base < 0 || cur.len > best.len))
|
|
3119
|
+
best = cur;
|
|
3120
|
+
if (best.base >= 0 && best.len < 2)
|
|
3121
|
+
best = { base: -1, len: 0 };
|
|
3122
|
+
const dotted = best.base === 0 && (best.len === 6 || (best.len === 5 && words[5] === 0xffff) || (best.len === 7 && words[7] !== 1));
|
|
3123
|
+
let out = "";
|
|
3124
|
+
for (let i = 0; i < 8; i++) {
|
|
3125
|
+
if (best.base >= 0 && i >= best.base && i < best.base + best.len) {
|
|
3126
|
+
if (i === best.base)
|
|
3127
|
+
out += "::";
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
if (i !== 0 && !out.endsWith(":"))
|
|
3131
|
+
out += ":";
|
|
3132
|
+
if (i === 6 && dotted) {
|
|
3133
|
+
out += addr.slice(12, 16).join(".");
|
|
3134
|
+
break;
|
|
3135
|
+
}
|
|
3136
|
+
out += words[i].toString(16);
|
|
3137
|
+
}
|
|
3138
|
+
return out;
|
|
3139
|
+
}
|
|
3140
|
+
/** an inet's output text: a full mask dropped (`/32` of an IPv4, `/128` of an IPv6), as inet_out writes it */
|
|
3141
|
+
const inetOut = (n) => fn("regexp_replace", [fn("regexp_replace", [n, strConst("^([0-9.]+)/32$"), strConst("\\1")]), strConst("^([0-9a-f:.]+)/128$"), strConst("\\1")]);
|
|
3142
|
+
/** An interval's datum: microseconds (int64), days, months (int32), little-endian; null for an infinite one, which
|
|
3143
|
+
* DuckDB has no counterpart of */
|
|
3144
|
+
export function intervalDatum(bytes) {
|
|
3145
|
+
if (bytes.length !== 16)
|
|
3146
|
+
return null;
|
|
3147
|
+
const le = (at, n) => { let v = 0n; for (let i = n - 1; i >= 0; i--)
|
|
3148
|
+
v = (v << 8n) | BigInt(bytes[at + i]); return BigInt.asIntN(n * 8, v); };
|
|
3149
|
+
const micros = le(0, 8), days = Number(le(8, 4)), months = Number(le(12, 4));
|
|
3150
|
+
if (months === 2147483647 || months === -2147483648)
|
|
3151
|
+
return null;
|
|
3152
|
+
return { months, days, micros };
|
|
3153
|
+
}
|
|
3154
|
+
export function datumText(type, bytes) {
|
|
3155
|
+
const le = (n) => { let v = 0n; for (let i = n - 1; i >= 0; i--)
|
|
3156
|
+
v = (v << 8n) | BigInt(bytes[i]); return BigInt.asIntN(n * 8, v); };
|
|
3157
|
+
const civil = (days) => {
|
|
3158
|
+
// days since 1970-01-01 → a proleptic Gregorian date (Howard Hinnant's civil_from_days)
|
|
3159
|
+
const z = days + 719468n, era = (z >= 0n ? z : z - 146096n) / 146097n, doe = z - era * 146097n;
|
|
3160
|
+
const yoe = (doe - doe / 1460n + doe / 36524n - doe / 146096n) / 365n, doy = doe - (365n * yoe + yoe / 4n - yoe / 100n);
|
|
3161
|
+
const mp = (5n * doy + 2n) / 153n, d = doy - (153n * mp + 2n) / 5n + 1n, m = mp < 10n ? mp + 3n : mp - 9n;
|
|
3162
|
+
const y = yoe + era * 400n + (m <= 2n ? 1n : 0n);
|
|
3163
|
+
const year = y < 0n ? `-${String(-y).padStart(4, "0")}` : String(y).padStart(4, "0");
|
|
3164
|
+
return `${year}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
3165
|
+
};
|
|
3166
|
+
if (type === INET) {
|
|
3167
|
+
// a varlena: a 4-byte header, the family (2 IPv4, 3 IPv6), the mask's bits, the address
|
|
3168
|
+
if (bytes.length < 10)
|
|
3169
|
+
return null;
|
|
3170
|
+
const family = bytes[4], bits = bytes[5], addr = bytes.slice(6);
|
|
3171
|
+
if (family === 2 && addr.length >= 4)
|
|
3172
|
+
return `${addr.slice(0, 4).join(".")}/${bits}`;
|
|
3173
|
+
if (family === 3 && addr.length >= 16)
|
|
3174
|
+
return `${ipv6Text(addr.slice(0, 16))}/${bits}`;
|
|
3175
|
+
return null;
|
|
3176
|
+
}
|
|
3177
|
+
if (type === DATE) {
|
|
3178
|
+
if (bytes.length !== 4)
|
|
3179
|
+
return null;
|
|
3180
|
+
const v = le(4);
|
|
3181
|
+
if (v === -2147483648n)
|
|
3182
|
+
return "-infinity";
|
|
3183
|
+
if (v === 2147483647n)
|
|
3184
|
+
return "infinity";
|
|
3185
|
+
return civil(v + 10957n);
|
|
3186
|
+
}
|
|
3187
|
+
if (bytes.length !== 8)
|
|
3188
|
+
return null;
|
|
3189
|
+
const v = le(8);
|
|
3190
|
+
if (v === -(1n << 63n))
|
|
3191
|
+
return "-infinity";
|
|
3192
|
+
if (v === (1n << 63n) - 1n)
|
|
3193
|
+
return "infinity";
|
|
3194
|
+
const micros = v + 946684800000000n, day = 86400000000n;
|
|
3195
|
+
const days = micros >= 0n ? micros / day : -((-micros + day - 1n) / day), rest = micros - days * day;
|
|
3196
|
+
const hh = rest / 3600000000n, mm = (rest / 60000000n) % 60n, ss = (rest / 1000000n) % 60n, us = rest % 1000000n;
|
|
3197
|
+
const time = `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}.${String(us).padStart(6, "0")}`;
|
|
3198
|
+
return `${civil(days)} ${time}${type === TIMESTAMPTZ ? "+00" : ""}`;
|
|
3199
|
+
}
|
|
3200
|
+
/** an array type whose elements' DuckDB text is Postgres's, and how each is written into json: smallint, integer, bigint
|
|
3201
|
+
* and boolean as they are, text, varchar and uuid as strings, jsonb canonical, json as written */
|
|
3202
|
+
const JSON_ARRAY_ELEMENT = { 1005: "r", 1007: "r", 1016: "r", 1000: "r", 1009: "s", 1015: "s", 2951: "s", 3807: "k", 199: "r" };
|
|
3203
|
+
const ARRAY_DUCK = { 1009: "varchar", 1015: "varchar", 1014: "varchar", 1007: "integer", 1016: "bigint", 1005: "smallint", 1022: "double", 1021: "real", 1000: "boolean", 1182: "date", 1115: "timestamp", 1185: "timestamptz", 2951: "uuid" };
|
|
3204
|
+
/** (a1, b1) OVERLAPS (a2, b2), as Postgres's overlaps_timestamp decides it, NULLs included: each period's ends in
|
|
3205
|
+
* order (a NULL start takes the end's place), then the SQL standard's cases */
|
|
3206
|
+
function overlaps(copy, a1, b1, a2, b2) {
|
|
3207
|
+
const start = (a, b) => caseWhen([[isNull(copy(a)), copy(b)], [and([isNotNull(copy(b)), op(">", copy(a), copy(b))]), copy(b)]], copy(a));
|
|
3208
|
+
const end = (a, b) => caseWhen([[isNull(copy(a)), nullConst()], [and([isNotNull(copy(b)), op(">", copy(a), copy(b))]), copy(a)]], copy(b));
|
|
3209
|
+
const s1 = () => start(a1, b1), e1 = () => end(a1, b1), s2 = () => start(a2, b2), e2 = () => end(a2, b2);
|
|
3210
|
+
const T = boolConst(true), F = () => boolConst(false);
|
|
3211
|
+
return caseWhen([
|
|
3212
|
+
[or([isNull(s1()), isNull(s2())]), nullConst()],
|
|
3213
|
+
[op(">", s1(), s2()), caseWhen([[isNull(e2()), nullConst()], [op("<", s1(), e2()), T], [isNull(e1()), nullConst()]], F())],
|
|
3214
|
+
[op("<", s1(), s2()), caseWhen([[isNull(e1()), nullConst()], [op("<", s2(), e1()), boolConst(true)], [isNull(e2()), nullConst()]], F())],
|
|
3215
|
+
], caseWhen([[or([isNull(e1()), isNull(e2())]), nullConst()]], boolConst(true)));
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* A json read by its steps (a JSON array of keys and indexes, src/duck/functions.ts jsonGet), native where DuckDB's
|
|
3219
|
+
* answer is Postgres's, the function's for the rest.
|
|
3220
|
+
*
|
|
3221
|
+
* THE FUNCTION IS A HOP TO THE MAIN THREAD PER VECTOR (the test server: 4.5 µs a row, a `->>` over 22 M
|
|
3222
|
+
* rows took 98 s and every other session, the stream and the commands waited for it). A `->>` / `#>>` of a column
|
|
3223
|
+
* is answered by DuckDB's own JSON reader where that reader answers exactly as Postgres does — a STRING value is the
|
|
3224
|
+
* same text from json_extract_string as from the function, a missing path (json_type's NULL) or a JSON null (its
|
|
3225
|
+
* 'NULL') is SQL NULL on both — and by
|
|
3226
|
+
* the function for every other value (a number, whose digits Postgres keeps as written where DuckDB writes `7.0`; an
|
|
3227
|
+
* object or an array, whose spacing is Postgres's): the CASE's ELSE, which DuckDB evaluates on those rows only
|
|
3228
|
+
* (measured: 10,000 number rows of 100,000 reached the function, in 49 vectors). Measured over 5 M rows: 3.85 s
|
|
3229
|
+
* through the function, 0.10 s native.
|
|
3230
|
+
* - Only a COLUMN's json is read natively (a chain of reads off one included, folded into one read: jsonChain): a literal
|
|
3231
|
+
* or an expression cast to json is validated by Postgres's rules inside the function, where DuckDB's parser would
|
|
3232
|
+
* refuse it in its own words.
|
|
3233
|
+
* - A document may hold a key twice, of which Postgres reads the LAST and DuckDB the first: the read is native only
|
|
3234
|
+
* where every object on the way holds each key once (json_keys at each step), unless `keysOnce` says the text holds
|
|
3235
|
+
* each once — jsonb's canonical text as the copy holds it (a copied table's column, Rewriter.jsonRead). A `json`
|
|
3236
|
+
* column is the document as written; a jsonb the statement computes (jsonb_object_agg, jsonb_build_object) is the
|
|
3237
|
+
* rewrite's text, a key twice included (a review finding).
|
|
3238
|
+
* - `j` is placed in every arm of the CASE, and each placement is its own copy (`copy`, the rewriter's clone): the
|
|
3239
|
+
* walker runs a node's rule once, so a node placed twice would be qualified or renamed in its first place alone
|
|
3240
|
+
* (`"Doc"` beside another table's `doc` was ambiguous to DuckDB, 42804; a review finding).
|
|
3241
|
+
* - Steps are keys only: an index, or a `#>` element that reads as one (pathIndex, the function's own definition: ` 1`
|
|
3242
|
+
* and `+1` are, `1 ` is not), stays the function's (a negative one counts from the end, and jsonb reads a scalar as
|
|
3243
|
+
* a one-element array). A pointer reads only an all-digit element as an index, so every other element is a key to
|
|
3244
|
+
* it, as to Postgres. The path is a JSON pointer (`/a/b`, `~` and `/`
|
|
3245
|
+
* escaped), which takes every key — except an all-digit key, which a pointer reads as an index on an array where
|
|
3246
|
+
* Postgres reads NULL: a path with one is JSONPath's quoted keys, which cannot say `*` (a wildcard even quoted)
|
|
3247
|
+
* or an empty key, so those stay the function's.
|
|
3248
|
+
* `->` (the json itself) stays the function's: a string's quoting and escapes are rewritten by DuckDB's writer.
|
|
3249
|
+
* tests/duck-wire.test.ts "a ->> of a column is DuckDB's own read" holds the answers and counts the hops.
|
|
3250
|
+
*/
|
|
3251
|
+
function jsonGet(copy, j, steps, either, canonical, asText, keysOnce) {
|
|
3252
|
+
const call = fn("__mason_json_get", [jsonArg(copy(j)), steps, boolConst(either), boolConst(canonical), boolConst(asText)]);
|
|
3253
|
+
if (!asText || kindOf(j) !== "ColumnRef")
|
|
3254
|
+
return call;
|
|
3255
|
+
const literal = constString(steps);
|
|
3256
|
+
if (literal === null)
|
|
3257
|
+
return call;
|
|
3258
|
+
const keys = [];
|
|
3259
|
+
for (const s of JSON.parse(literal)) {
|
|
3260
|
+
// a `->` step that is an integer is an index; a `#>` element that reads as one is an index on an array, as the
|
|
3261
|
+
// function reads it (pathIndex: `+1` and ` 1` are, `1 ` is a key)
|
|
3262
|
+
if (typeof s !== "string" || (either && pathIndex(s) !== null) || /[\u0000-\u001f]/.test(s))
|
|
3263
|
+
return call;
|
|
3264
|
+
keys.push(s);
|
|
3265
|
+
}
|
|
3266
|
+
if (keys.length === 0)
|
|
3267
|
+
return call;
|
|
3268
|
+
const pointer = (ks) => ks.map((k) => "/" + k.replaceAll("~", "~0").replaceAll("/", "~1")).join("");
|
|
3269
|
+
let path;
|
|
3270
|
+
if (keys.some((k) => /^\d+$/.test(k))) {
|
|
3271
|
+
if (keys.some((k) => k === "" || k === "*"))
|
|
3272
|
+
return call;
|
|
3273
|
+
path = "$" + keys.map((k) => `."${k.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`).join("");
|
|
3274
|
+
}
|
|
3275
|
+
else
|
|
3276
|
+
path = pointer(keys);
|
|
3277
|
+
const type = () => fn("json_type", [copy(j), strConst(path)]);
|
|
3278
|
+
// each object on the way holds every key once (json_keys of an array or a scalar is empty, of a missing path NULL:
|
|
3279
|
+
// the read there is NULL on both sides, so both pass)
|
|
3280
|
+
// (made again for each arm that tests it: every node is placed once)
|
|
3281
|
+
const once = () => keysOnce ? [] : keys.map((_, i) => {
|
|
3282
|
+
const at = () => fn("json_keys", i === 0 ? [copy(j)] : [copy(j), strConst(pointer(keys.slice(0, i)))]);
|
|
3283
|
+
return { CoalesceExpr: { args: [op("=", fn("len", [at()]), fn("len", [fn("list_distinct", [at()])])), boolConst(true)] } };
|
|
3284
|
+
});
|
|
3285
|
+
const when = (test) => keysOnce ? test : and([...once(), test]);
|
|
3286
|
+
return caseWhen([
|
|
3287
|
+
// (a NULL document first: its json_keys is NULL, which no check passes)
|
|
3288
|
+
[isNull(copy(j)), nullConst()],
|
|
3289
|
+
[when(op("=", type(), strConst("VARCHAR"))), fn("json_extract_string", [copy(j), strConst(path)])],
|
|
3290
|
+
// a missing path is SQL NULL from json_type, a JSON null its name
|
|
3291
|
+
[when(or([isNull(type()), op("=", type(), strConst("NULL"))])), nullConst()],
|
|
3292
|
+
], call);
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* A STORED PATH'S `->>`, FROM ITS COLUMN ALONE (Rewriter.keptRead): the column holds the `->` answer — the value's JSON
|
|
3296
|
+
* text — or `KEPT_NULL`, and `->>` is that text where the value is not a string (a number's text is the same both ways,
|
|
3297
|
+
* a jsonb's numeric included; an object or an array its text), NULL for a JSON null or no value, and a string's
|
|
3298
|
+
* characters: its text between the quotes where it holds no escape, else the function's `->>` of the value's own text
|
|
3299
|
+
* (`[]` steps) — the same function on the same value, only for the rows that take that arm, never the document.
|
|
3300
|
+
*/
|
|
3301
|
+
function keptText(column, canonical) {
|
|
3302
|
+
return caseWhen([
|
|
3303
|
+
[or([op("=", column(), fn("chr", [intConst(0)])), op("=", column(), strConst("null"))]), nullConst()],
|
|
3304
|
+
[fn("starts_with", [column(), strConst('"')]), caseWhen([
|
|
3305
|
+
[fn("contains", [column(), strConst("\\")]), fn("__mason_json_get", [column(), strConst("[]"), boolConst(false), boolConst(canonical), boolConst(true)])],
|
|
3306
|
+
], fn("substr", [column(), intConst(2), op("-", fn("length", [column()]), intConst(2))]))],
|
|
3307
|
+
], column());
|
|
3308
|
+
}
|
|
3309
|
+
/** `->`'s key (an integer one an index) or json_extract_path's elements, as the JSON array of steps */
|
|
3310
|
+
function jsonSteps(keys, index) {
|
|
3311
|
+
const literal = keys.map((k) => index ? constInt(k) : constString(k));
|
|
3312
|
+
if (literal.every((k) => k !== null))
|
|
3313
|
+
return strConst(JSON.stringify(literal));
|
|
3314
|
+
return cast(fn("to_json", [fn("list_value", keys.map((k) => cast(k, [index ? "bigint" : "varchar"])))]), ["varchar"]);
|
|
3315
|
+
}
|
|
3316
|
+
/** Postgres's text-array element types, as DuckDB's */
|
|
3317
|
+
const ARRAY_ELEMENT = { text: "varchar", varchar: "varchar", int4: "integer", int: "integer", integer: "integer", int8: "bigint", bigint: "bigint", int2: "smallint", smallint: "smallint", float8: "double", numeric: "double", bool: "boolean", boolean: "boolean", date: "date" };
|
|
3318
|
+
/** an array literal's elements as array_in reads them: quotes and backslashes, an unquoted NULL is NULL */
|
|
3319
|
+
function arrayLiteral(text) {
|
|
3320
|
+
const inner = text.trim();
|
|
3321
|
+
if (!inner.startsWith("{") || !inner.endsWith("}"))
|
|
3322
|
+
throw new Unrewritable(`malformed array literal: "${text}"`, 'write it as \'{a,b}\'', { postgres: "22P02" });
|
|
3323
|
+
const body = inner.slice(1, -1);
|
|
3324
|
+
if (body.trim() === "")
|
|
3325
|
+
return [];
|
|
3326
|
+
const out = [];
|
|
3327
|
+
let cur = "", quoted = false, wasQuoted = false;
|
|
3328
|
+
for (let i = 0; i < body.length; i++) {
|
|
3329
|
+
const c = body[i];
|
|
3330
|
+
if (quoted) {
|
|
3331
|
+
if (c === "\\") {
|
|
3332
|
+
cur += body[++i] ?? "";
|
|
3333
|
+
continue;
|
|
3334
|
+
}
|
|
3335
|
+
if (c === '"') {
|
|
3336
|
+
quoted = false;
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
cur += c;
|
|
3340
|
+
continue;
|
|
3341
|
+
}
|
|
3342
|
+
if (c === "{")
|
|
3343
|
+
throw new Unrewritable("a multi-dimensional array literal is not supported by Mason", "write a one-dimensional array", "multidim-array");
|
|
3344
|
+
if (c === '"') {
|
|
3345
|
+
quoted = true;
|
|
3346
|
+
wasQuoted = true;
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
if (c === "\\") {
|
|
3350
|
+
cur += body[++i] ?? "";
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
if (c === ",") {
|
|
3354
|
+
out.push(!wasQuoted && cur.trim().toUpperCase() === "NULL" ? null : wasQuoted ? cur : cur.trim());
|
|
3355
|
+
cur = "";
|
|
3356
|
+
wasQuoted = false;
|
|
3357
|
+
continue;
|
|
3358
|
+
}
|
|
3359
|
+
cur += c;
|
|
3360
|
+
}
|
|
3361
|
+
out.push(!wasQuoted && cur.trim().toUpperCase() === "NULL" ? null : wasQuoted ? cur : cur.trim());
|
|
3362
|
+
return out;
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* A SIMILAR TO pattern as RE2's, as Postgres's similar_to_escape translates it: `%` any text, `_` one character, the
|
|
3366
|
+
* escape character's next character itself, `.` `^` `$` themselves, and the regular-expression operators (`|` `*` `+`
|
|
3367
|
+
* `?` `{…}` `(…)` `[…]`) as they are. The whole text must match (regexp_full_match).
|
|
3368
|
+
*/
|
|
3369
|
+
export function similarToRe2(pattern, escape) {
|
|
3370
|
+
if (escape.length > 1)
|
|
3371
|
+
throw new Unrewritable("invalid escape string", "Escape string must be empty or one character.", { postgres: "22025" });
|
|
3372
|
+
let out = "", inClass = false;
|
|
3373
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
3374
|
+
const c = pattern[i];
|
|
3375
|
+
if (escape !== "" && c === escape) {
|
|
3376
|
+
const next = pattern[++i];
|
|
3377
|
+
if (next === undefined)
|
|
3378
|
+
throw new Unrewritable("invalid escape at the end of a SIMILAR TO pattern", "escape a character, or end the pattern without the escape character", { postgres: "22025" });
|
|
3379
|
+
if (next === '"')
|
|
3380
|
+
throw new Unrewritable("SIMILAR TO's substring markers (escape + \") are not supported by Mason", "use substring(text from pattern) with a POSIX regular expression", "similar-to-forms");
|
|
3381
|
+
out += /[A-Za-z0-9]/.test(next) ? next : `\\${next}`;
|
|
3382
|
+
continue;
|
|
3383
|
+
}
|
|
3384
|
+
if (inClass) {
|
|
3385
|
+
out += c === "\\" ? "\\\\" : c;
|
|
3386
|
+
if (c === "]")
|
|
3387
|
+
inClass = false;
|
|
3388
|
+
continue;
|
|
3389
|
+
}
|
|
3390
|
+
if (c === "[") {
|
|
3391
|
+
inClass = true;
|
|
3392
|
+
out += c;
|
|
3393
|
+
if (pattern[i + 1] === "^") {
|
|
3394
|
+
out += "^";
|
|
3395
|
+
i++;
|
|
3396
|
+
}
|
|
3397
|
+
if (pattern[i + 1] === "]") {
|
|
3398
|
+
out += "\\]";
|
|
3399
|
+
i++;
|
|
3400
|
+
}
|
|
3401
|
+
continue;
|
|
3402
|
+
}
|
|
3403
|
+
if (c === "%") {
|
|
3404
|
+
out += ".*";
|
|
3405
|
+
continue;
|
|
3406
|
+
}
|
|
3407
|
+
if (c === "_") {
|
|
3408
|
+
out += ".";
|
|
3409
|
+
continue;
|
|
3410
|
+
}
|
|
3411
|
+
if (c === "(") {
|
|
3412
|
+
out += pattern[i + 1] === "?" ? "(" : "(?:";
|
|
3413
|
+
continue;
|
|
3414
|
+
}
|
|
3415
|
+
if (".^$\\".includes(c)) {
|
|
3416
|
+
out += `\\${c}`;
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
out += c;
|
|
3420
|
+
}
|
|
3421
|
+
return out;
|
|
3422
|
+
}
|
|
3423
|
+
/** the parenthesised groups of a regular expression that capture (`(?:…)` does not; inside […] a parenthesis is a character) */
|
|
3424
|
+
function captureGroups(pattern) {
|
|
3425
|
+
let n = 0, inClass = false;
|
|
3426
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
3427
|
+
const c = pattern[i];
|
|
3428
|
+
if (c === "\\") {
|
|
3429
|
+
i++;
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
if (inClass) {
|
|
3433
|
+
if (c === "]")
|
|
3434
|
+
inClass = false;
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
if (c === "[") {
|
|
3438
|
+
inClass = true;
|
|
3439
|
+
if (pattern[i + 1] === "^")
|
|
3440
|
+
i++;
|
|
3441
|
+
if (pattern[i + 1] === "]")
|
|
3442
|
+
i++;
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
if (c === "(" && pattern[i + 1] !== "?")
|
|
3446
|
+
n++;
|
|
3447
|
+
}
|
|
3448
|
+
return n;
|
|
3449
|
+
}
|
|
3450
|
+
/** pg_catalog.interval's field masks (INTERVAL_MASK): the lowest field a typmod names, finest first */
|
|
3451
|
+
const INTERVAL_FIELDS = [[1 << 12, "second"], [1 << 11, "minute"], [1 << 10, "hour"], [1 << 3, "day"], [1 << 1, "month"], [1 << 2, "year"]];
|
|
3452
|
+
/** `interval '3' day`, `x::interval hour`: a bare number is a count of the lowest field, and the fields below it are
|
|
3453
|
+
* cut off (AdjustIntervalForTypmod), as date_trunc cuts an interval */
|
|
3454
|
+
function intervalFields(arg, mask) {
|
|
3455
|
+
const unit = INTERVAL_FIELDS.find(([bit]) => mask !== null && (mask & bit) !== 0)?.[1] ?? "second";
|
|
3456
|
+
const s = constString(arg);
|
|
3457
|
+
const value = s !== null && /^\s*[+-]?(\d+\.?\d*|\.\d+)\s*$/.test(s) ? strConst(`${s.trim()} ${unit}s`) : arg;
|
|
3458
|
+
const iv = cast(value, ["interval"]);
|
|
3459
|
+
return unit === "second" ? iv : fn("date_trunc", [strConst(unit), iv]);
|
|
3460
|
+
}
|
|
3461
|
+
/** a json_to_record column definition's type, spelled for Postgres, and how its value comes back to DuckDB (the
|
|
3462
|
+
* worker's kind and the DuckDB type it is cast to); `read` null for a type not read here (an array, a type of a
|
|
3463
|
+
* schema's own) */
|
|
3464
|
+
function recordColumn(t) {
|
|
3465
|
+
const name = lastName(t?.names);
|
|
3466
|
+
const mods = (t?.typmods ?? []).map((m) => constInt(m));
|
|
3467
|
+
const spelled = `${name}${mods.length > 0 ? `(${mods.join(", ")})` : ""}${Array.isArray(t?.arrayBounds) && t.arrayBounds.length > 0 ? "[]" : ""}`;
|
|
3468
|
+
if (spelled.endsWith("]") || mods.some((m) => m === null) || (t?.names ?? []).length > 2)
|
|
3469
|
+
return { spelled, read: null };
|
|
3470
|
+
const reads = {
|
|
3471
|
+
int2: ["int8", ["smallint"]], int4: ["int8", ["integer"]], int8: ["int8", null], bool: ["bool", null], date: ["date", null],
|
|
3472
|
+
timestamp: ["timestamp", null], timestamptz: ["timestamptz", null], interval: ["interval", null], text: ["text", null],
|
|
3473
|
+
varchar: ["text", null], bpchar: ["text", null], float8: ["text", ["double"]], float4: ["text", ["float"]], uuid: ["text", ["uuid"]],
|
|
3474
|
+
json: ["text", ["json"]], jsonb: ["text", ["json"]],
|
|
3475
|
+
numeric: ["text", mods.length > 0 && mods[0] <= 38 ? ["decimal", String(mods[0]), String(mods[1] ?? 0)] : ["double"]],
|
|
3476
|
+
};
|
|
3477
|
+
return { spelled, read: reads[name] ?? null };
|
|
3478
|
+
}
|
|
3479
|
+
/** json_agg's text of items each written with its own line break before it (`\n {…}`, `null`): `[` the items joined by
|
|
3480
|
+
* `, ` `]`, the first item's line break dropped, as json_agg_transfn writes none before the first */
|
|
3481
|
+
function linedAgg(b, item, done) {
|
|
3482
|
+
const agg = { FuncCall: { ...b, funcname: [S("string_agg")], args: [item, strConst(", ")] } };
|
|
3483
|
+
done.add(bodyOf(agg));
|
|
3484
|
+
return concat(strConst("["), fn("regexp_replace", [agg, strConst("^\n "), strConst("")]), strConst("]"));
|
|
3485
|
+
}
|
|
3486
|
+
/** the divisor of a float or numeric division: Postgres raises on zero */
|
|
3487
|
+
function nonZero(copy, r) {
|
|
3488
|
+
// a constant, or a constant the rewrite cast (`CAST(2 AS double)`): no guard, so no volatile error() call, which DuckDB
|
|
3489
|
+
// refuses in a DISTINCT aggregate's ORDER BY
|
|
3490
|
+
let c = r;
|
|
3491
|
+
while (kindOf(c) === "TypeCast" && isNode(bodyOf(c).arg))
|
|
3492
|
+
c = bodyOf(c).arg;
|
|
3493
|
+
if (kindOf(c) === "A_Const") {
|
|
3494
|
+
const b = bodyOf(c);
|
|
3495
|
+
const v = b.ival !== undefined ? Number(b.ival.ival ?? 0) : b.fval !== undefined ? Number(b.fval.fval) : null;
|
|
3496
|
+
if (v !== null && v !== 0)
|
|
3497
|
+
return r;
|
|
3498
|
+
}
|
|
3499
|
+
// NULL in the branch no row takes: the CASE keeps the divisor's type
|
|
3500
|
+
return caseWhen([[fn("__mason_zero_divisor", [op("=", r, intConst(0))]), nullConst()]], copy(r));
|
|
3501
|
+
}
|
|
3502
|
+
/** a decimal text without an exponent: `1.5e-3` → `0.0015` */
|
|
3503
|
+
function positional(text) {
|
|
3504
|
+
const m = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(text);
|
|
3505
|
+
if (m === null)
|
|
3506
|
+
return text;
|
|
3507
|
+
const digits = `${m[2] ?? ""}${m[3] ?? ""}`;
|
|
3508
|
+
const point = (m[2] ?? "").length + Number(m[4] ?? 0);
|
|
3509
|
+
let whole, frac;
|
|
3510
|
+
if (point <= 0) {
|
|
3511
|
+
whole = "0";
|
|
3512
|
+
frac = "0".repeat(-point) + digits;
|
|
3513
|
+
}
|
|
3514
|
+
else if (point >= digits.length) {
|
|
3515
|
+
whole = digits + "0".repeat(point - digits.length);
|
|
3516
|
+
frac = "";
|
|
3517
|
+
}
|
|
3518
|
+
else {
|
|
3519
|
+
whole = digits.slice(0, point);
|
|
3520
|
+
frac = digits.slice(point);
|
|
3521
|
+
}
|
|
3522
|
+
whole = whole.replace(/^0+(?=\d)/, "") || "0";
|
|
3523
|
+
return `${m[1] === "-" ? "-" : ""}${whole}${frac === "" ? "" : `.${frac}`}`;
|
|
3524
|
+
}
|
|
3525
|
+
/** a numeric constant written positionally, as DuckDB's DECIMAL: `0.025` reads as one, but a whole `250` (from `2.5e2`)
|
|
3526
|
+
* would read back as an INTEGER — integer arithmetic, an overflow where Postgres's numeric has none — so it is cast,
|
|
3527
|
+
* as an integer cast to numeric is, to DECIMAL(38, 0) (DuckDB keeps a product of two 18-digit decimals in 64 bits and
|
|
3528
|
+
* fails past it). Past DuckDB's 38 digits it stays as written: DuckDB's double, as before */
|
|
3529
|
+
function decimalConst(text) {
|
|
3530
|
+
if (text.includes(".") || text.replace(/^-/, "").length > 38)
|
|
3531
|
+
return numConst(text);
|
|
3532
|
+
const n = Number(text);
|
|
3533
|
+
return cast(Number.isSafeInteger(n) && n >= -2147483648 && n <= 2147483647 ? intConst(n) : numConst(text), ["decimal", "38", "0"]);
|
|
3534
|
+
}
|
|
3535
|
+
/** a text cast to an integer, as strict as Postgres's: whitespace around an optional sign and digits, nothing else */
|
|
3536
|
+
/** the jsonpath functions' arguments: the target, the path, the variables, `silent` */
|
|
3537
|
+
const JSONPATH_ARGS = ["jsonb", "jsonpath", "jsonb", "boolean"];
|
|
3538
|
+
/** Postgres's own answer for each row (src/duck/pg-eval.ts): `template` over a1…an, each argument's text, under the
|
|
3539
|
+
* zone DuckDB's session has */
|
|
3540
|
+
function pgCall(kind, template, args) {
|
|
3541
|
+
return fn(`__mason_pg_${kind}`, [fn("current_setting", [strConst("TimeZone")]), strConst(template), ...args.map((a) => cast(a, ["varchar"]))]);
|
|
3542
|
+
}
|
|
3543
|
+
/** a json argument: a text literal is read as JSON (DuckDB's json functions over a VARCHAR literal parse it anyway) */
|
|
3544
|
+
const jsonArg = (n) => n;
|
|
3545
|
+
/** `#>` / `#>>`'s path (a text array: a literal `'{a,b,1}'` or an expression) as the JSON array of its elements */
|
|
3546
|
+
function jsonPath(r) {
|
|
3547
|
+
const s = constString(r);
|
|
3548
|
+
if (s === null)
|
|
3549
|
+
return cast(fn("to_json", [r]), ["varchar"]);
|
|
3550
|
+
const elements = arrayLiteral(s);
|
|
3551
|
+
if (elements.some((e) => e === null))
|
|
3552
|
+
return nullConst();
|
|
3553
|
+
return strConst(JSON.stringify(elements));
|
|
3554
|
+
}
|
|
3555
|
+
/**
|
|
3556
|
+
* A Postgres (ARE) pattern as RE2's: `.` matches a newline (Postgres's default; RE2's `(?s)`), `\w` / `\d` / `\s` are
|
|
3557
|
+
* Unicode's (RE2's are ASCII), `\Z` is the end (`\z`). Null where RE2 would not answer as Postgres's engine does, which
|
|
3558
|
+
* then answers it (src/duck/pg-eval.ts): a pattern that is not a literal; a construct RE2 lacks or reads otherwise — a
|
|
3559
|
+
* lookaround, a back reference, embedded options, a word boundary or a word's start or end (`\y`, `\m`, `\M`: RE2's
|
|
3560
|
+
* `\b` knows ASCII words alone); and, `extract`, a pattern whose MATCH (not whether there is one) depends on the
|
|
3561
|
+
* engine: alternation, or more than one quantifier (Postgres takes the longest match, RE2 the first: `substring('aab'
|
|
3562
|
+
* from 'a*(ab)?')` is `aab` there, `aa` in RE2).
|
|
3563
|
+
*/
|
|
3564
|
+
function regexPattern(p, extract = false) {
|
|
3565
|
+
const s = constString(p);
|
|
3566
|
+
if (s === null)
|
|
3567
|
+
return null;
|
|
3568
|
+
const re = areToRe2(s, extract);
|
|
3569
|
+
return re === null ? null : strConst(`(?s)${re}`);
|
|
3570
|
+
}
|
|
3571
|
+
export function areToRe2(s, extract = false) {
|
|
3572
|
+
if (s.startsWith("***"))
|
|
3573
|
+
return null;
|
|
3574
|
+
let out = "", inClass = false, quantifiers = 0;
|
|
3575
|
+
for (let i = 0; i < s.length; i++) {
|
|
3576
|
+
const c = s[i];
|
|
3577
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
3578
|
+
const e = s[i + 1];
|
|
3579
|
+
i++;
|
|
3580
|
+
const cls = { w: "\\p{L}\\p{Nd}_", d: "\\p{Nd}", s: "\\s" }[e];
|
|
3581
|
+
if (cls !== undefined) {
|
|
3582
|
+
out += inClass ? cls : `[${cls}]`;
|
|
3583
|
+
continue;
|
|
3584
|
+
}
|
|
3585
|
+
const neg = { W: "\\p{L}\\p{Nd}_", D: "\\p{Nd}" }[e];
|
|
3586
|
+
if (neg !== undefined && !inClass) {
|
|
3587
|
+
out += `[^${neg}]`;
|
|
3588
|
+
continue;
|
|
3589
|
+
}
|
|
3590
|
+
// a word boundary, start or end; a back reference (\1, \k<…>): Postgres's engine
|
|
3591
|
+
if (!inClass && /[yYmM1-9k]/.test(e))
|
|
3592
|
+
return null;
|
|
3593
|
+
if (!inClass && e === "Z") {
|
|
3594
|
+
out += "\\z";
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
out += `\\${e}`;
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
if (c === "[" && !inClass) {
|
|
3601
|
+
inClass = true;
|
|
3602
|
+
out += c;
|
|
3603
|
+
if (s[i + 1] === "^") {
|
|
3604
|
+
out += "^";
|
|
3605
|
+
i++;
|
|
3606
|
+
}
|
|
3607
|
+
if (s[i + 1] === "]") {
|
|
3608
|
+
out += "\\]";
|
|
3609
|
+
i++;
|
|
3610
|
+
}
|
|
3611
|
+
continue;
|
|
3612
|
+
}
|
|
3613
|
+
if (c === "]" && inClass) {
|
|
3614
|
+
inClass = false;
|
|
3615
|
+
out += c;
|
|
3616
|
+
continue;
|
|
3617
|
+
}
|
|
3618
|
+
if (!inClass) {
|
|
3619
|
+
// a lookaround, embedded options, a comment: everything after `(?` but a group that only groups
|
|
3620
|
+
if (c === "(" && s[i + 1] === "?" && s[i + 2] !== ":")
|
|
3621
|
+
return null;
|
|
3622
|
+
if (c === "|")
|
|
3623
|
+
quantifiers += 2;
|
|
3624
|
+
// a group that may match nothing (`(b)?`, `(b)*`): RE2's extract has no unmatched group ('' where Postgres has NULL)
|
|
3625
|
+
if (extract && (c === "?" || c === "*" || c === "{") && s[i - 1] === ")")
|
|
3626
|
+
return null;
|
|
3627
|
+
// a quantifier (a `?` after one makes it lazy, and is none of its own)
|
|
3628
|
+
if (c === "*" || c === "+" || c === "{" || (c === "?" && !/[*+?}]/.test(s[i - 1] ?? "") && s[i - 1] !== "("))
|
|
3629
|
+
quantifiers++;
|
|
3630
|
+
}
|
|
3631
|
+
out += c;
|
|
3632
|
+
}
|
|
3633
|
+
return extract && quantifiers > 1 ? null : out;
|
|
3634
|
+
}
|
|
3635
|
+
/** regexp_replace's replacement: Postgres's `\&` is the whole match, RE2's `\0` */
|
|
3636
|
+
function replacement(r) {
|
|
3637
|
+
const s = constString(r);
|
|
3638
|
+
if (s === null)
|
|
3639
|
+
return fn("replace", [r, strConst("\\&"), strConst("\\0")]);
|
|
3640
|
+
return strConst(s.replace(/\\(.)/g, (_m, c) => c === "&" ? "\\0" : `\\${c}`));
|
|
3641
|
+
}
|
|
3642
|
+
/** substring(text FROM pattern): the first parenthesised part when there is one, else the whole match; NULL without one */
|
|
3643
|
+
function regexSubstring(copy, s, p) {
|
|
3644
|
+
const pattern = regexPattern(p, true);
|
|
3645
|
+
const text = constString(p);
|
|
3646
|
+
const group = text !== null && /\((?!\?)/.test(text.replace(/\\./g, "")) ? 1 : 0;
|
|
3647
|
+
return caseWhen([[fn("regexp_matches", [copy(s), copy(pattern)]), fn("regexp_extract", [s, pattern, intConst(group)])]], null);
|
|
3648
|
+
}
|
|
3649
|
+
/** make_interval(years, months, weeks, days, hours, mins, secs), positional or named, as DuckDB's interval sums */
|
|
3650
|
+
function makeInterval(args) {
|
|
3651
|
+
const order = ["years", "months", "weeks", "days", "hours", "mins", "secs"];
|
|
3652
|
+
const parts = new Map();
|
|
3653
|
+
args.forEach((a, i) => {
|
|
3654
|
+
if (kindOf(a) === "NamedArgExpr")
|
|
3655
|
+
parts.set(String(bodyOf(a).name), bodyOf(a).arg);
|
|
3656
|
+
else
|
|
3657
|
+
parts.set(order[i], a);
|
|
3658
|
+
});
|
|
3659
|
+
const terms = [];
|
|
3660
|
+
const add = (key, f) => { const x = parts.get(key); if (x !== undefined)
|
|
3661
|
+
terms.push(f(x)); };
|
|
3662
|
+
add("years", (x) => fn("to_years", [cast(x, ["integer"])]));
|
|
3663
|
+
add("months", (x) => fn("to_months", [cast(x, ["integer"])]));
|
|
3664
|
+
add("weeks", (x) => fn("to_days", [op("*", cast(x, ["integer"]), intConst(7))]));
|
|
3665
|
+
add("days", (x) => fn("to_days", [cast(x, ["integer"])]));
|
|
3666
|
+
add("hours", (x) => fn("to_hours", [cast(x, ["bigint"])]));
|
|
3667
|
+
add("mins", (x) => fn("to_minutes", [cast(x, ["bigint"])]));
|
|
3668
|
+
add("secs", (x) => fn("to_microseconds", [cast(fn("round", [op("*", cast(x, ["double"]), numConst("1000000.0"))]), ["bigint"])]));
|
|
3669
|
+
if (terms.length === 0)
|
|
3670
|
+
return cast(strConst("0 seconds"), ["interval"]);
|
|
3671
|
+
return terms.reduce((a, b) => op("+", a, b));
|
|
3672
|
+
}
|
|
3673
|
+
/** jsonb_typeof: DuckDB's json_type in Postgres's words */
|
|
3674
|
+
function jsonTypeof(x) {
|
|
3675
|
+
const t = fn("json_type", [x]);
|
|
3676
|
+
return { CaseExpr: { arg: t, args: [["OBJECT", "object"], ["ARRAY", "array"], ["VARCHAR", "string"], ["BIGINT", "number"], ["UBIGINT", "number"], ["DOUBLE", "number"], ["HUGEINT", "number"], ["BOOLEAN", "boolean"], ["NULL", "null"]]
|
|
3677
|
+
.map(([w, r]) => ({ CaseWhen: { expr: strConst(w), result: strConst(r) } })) } };
|
|
3678
|
+
}
|
|
3679
|
+
/** format(fmt, …) with only `%s` and `%%`: DuckDB's printf, each value as its text and NULL as nothing (Postgres's) */
|
|
3680
|
+
function formatCall(args, text) {
|
|
3681
|
+
const fmt = constString(args[0]);
|
|
3682
|
+
if (fmt === null || /%(?![s%])/.test(fmt))
|
|
3683
|
+
throw new Unrewritable("format() with a format other than %s is not supported by Mason", "concatenate the parts with ||, or use only %s", "format-specifiers");
|
|
3684
|
+
return fn("printf", [strConst(fmt), ...args.slice(1).map((a, i) => ({ CoalesceExpr: { args: [text(a, i + 1), strConst("")] } }))]);
|
|
3685
|
+
}
|
|
3686
|
+
/** Postgres's to_char / to_timestamp template patterns, as strftime / strptime's (null where strftime has none) */
|
|
3687
|
+
const TEMPLATE = [
|
|
3688
|
+
["HH24", "%H"], ["HH12", "%I"], ["HH", "%I"], ["MI", "%M"], ["SS", "%S"], ["MS", "%g"], ["US", "%f"],
|
|
3689
|
+
["AM", "%p"], ["PM", "%p"], ["am", "%p"], ["pm", "%p"],
|
|
3690
|
+
["YYYY", "%Y"], ["IYYY", "%G"], ["YY", "%y"], ["MONTH", null], ["Month", "%B"], ["month", null], ["MON", null], ["Mon", "%b"], ["mon", null], ["MM", "%m"],
|
|
3691
|
+
["DAY", null], ["Day", "%A"], ["day", null], ["DY", null], ["Dy", "%a"], ["dy", null], ["DDD", "%j"], ["DD", "%d"], ["IW", "%V"], ["TZ", "%Z"],
|
|
3692
|
+
];
|
|
3693
|
+
function translateTemplate(fmt, forParse) {
|
|
3694
|
+
let out = "", i = 0;
|
|
3695
|
+
while (i < fmt.length) {
|
|
3696
|
+
if (fmt[i] === '"') {
|
|
3697
|
+
const end = fmt.indexOf('"', i + 1);
|
|
3698
|
+
const lit = fmt.slice(i + 1, end < 0 ? undefined : end);
|
|
3699
|
+
out += lit.replace(/%/g, "%%");
|
|
3700
|
+
i = end < 0 ? fmt.length : end + 1;
|
|
3701
|
+
continue;
|
|
3702
|
+
}
|
|
3703
|
+
if (fmt.startsWith("FM", i)) {
|
|
3704
|
+
i += 2;
|
|
3705
|
+
continue;
|
|
3706
|
+
}
|
|
3707
|
+
const hit = TEMPLATE.find(([p]) => fmt.startsWith(p, i));
|
|
3708
|
+
if (hit !== undefined) {
|
|
3709
|
+
const [pattern, spec] = hit;
|
|
3710
|
+
// Month / Day pad to 9 characters in Postgres unless FM: refused there rather than answered unpadded
|
|
3711
|
+
const padded = (pattern === "Month" || pattern === "Day") && !fmt.slice(Math.max(0, i - 2), i).startsWith("FM");
|
|
3712
|
+
if (spec === null || (padded && !forParse))
|
|
3713
|
+
throw new Unrewritable(`the to_char pattern ${pattern}${padded ? " (padded to 9 characters)" : ""} is not supported by Mason`, padded ? `write FM${pattern} for the unpadded name` : "use YYYY MM DD HH24 MI SS MS US Mon Month Dy Day IYYY IW DDD AM", "to-char-patterns");
|
|
3714
|
+
out += spec;
|
|
3715
|
+
i += pattern.length;
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
if (/[A-Za-z]/.test(fmt[i])) {
|
|
3719
|
+
const word = /^[A-Za-z]+/.exec(fmt.slice(i))[0];
|
|
3720
|
+
throw new Unrewritable(`the to_char pattern ${word} is not supported by Mason`, "use YYYY MM DD HH24 MI SS MS US Mon Month Dy Day IYYY IW DDD AM, or put literal text in double quotes", "to-char-patterns");
|
|
3721
|
+
}
|
|
3722
|
+
out += fmt[i] === "%" ? "%%" : fmt[i];
|
|
3723
|
+
i++;
|
|
3724
|
+
}
|
|
3725
|
+
return out;
|
|
3726
|
+
}
|
|
3727
|
+
/** to_char of a time or a number, by Postgres's template rules (src/duck/to-char.ts): the value's text, the format's */
|
|
3728
|
+
function toChar(args, type) {
|
|
3729
|
+
if (args.length !== 2)
|
|
3730
|
+
throw new Unrewritable("to_char() takes a value and a format", "write to_char(value, 'format')", { postgres: "42883" });
|
|
3731
|
+
if (type !== null && [DATE, TIMESTAMP, TIMESTAMPTZ].includes(type))
|
|
3732
|
+
return fn("__mason_to_char_time", [cast(args[0], ["varchar"]), cast(args[1], ["varchar"])]);
|
|
3733
|
+
if (type !== null && (INTEGERS.has(type) || FLOATS.has(type) || type === NUMERIC))
|
|
3734
|
+
return fn("__mason_to_char_number", [cast(args[0], ["varchar"]), cast(args[1], ["varchar"])]);
|
|
3735
|
+
throw new Unrewritable("to_char of an interval is not supported by Mason", "format its parts with extract(… from interval), or to_char a timestamp", "to-char-patterns");
|
|
3736
|
+
}
|
|
3737
|
+
function strptimeFormat(fmt) {
|
|
3738
|
+
const s = constString(fmt);
|
|
3739
|
+
if (s === null)
|
|
3740
|
+
throw new Unrewritable("to_timestamp / to_date with a format that is not a literal is not supported by Mason", "write the format as a string literal", "to-timestamp-format");
|
|
3741
|
+
return translateTemplate(s, true);
|
|
3742
|
+
}
|
|
3743
|
+
/** Rewrites one statement's raw tree (libpg-query's `stmt`, offsets from `base`) by Postgres's analysis of it. */
|
|
3744
|
+
/**
|
|
3745
|
+
* The name Postgres gives a select-list expression that has no AS (FigureColname, parse_target.c): a column's own
|
|
3746
|
+
* name, a function's, `case`, `coalesce`, `array`, `exists`, a cast's argument's name or its type's, and `?column?` for
|
|
3747
|
+
* the rest. DuckDB names them otherwise (`count_star()`, `(1 + 1)`, `CAST(x AS VARCHAR(40))`), so a query reading an
|
|
3748
|
+
* inner query's column by Postgres's name failed there (a review finding: `SELECT s.count FROM (SELECT count(*)
|
|
3749
|
+
* …) s`). Every unnamed target is given this name (`nameTargets`).
|
|
3750
|
+
*/
|
|
3751
|
+
export function figureColname(n) {
|
|
3752
|
+
const k = kindOf(n), b = bodyOf(n);
|
|
3753
|
+
switch (k) {
|
|
3754
|
+
case "ColumnRef": {
|
|
3755
|
+
const f = (b.fields ?? []);
|
|
3756
|
+
const last = f[f.length - 1];
|
|
3757
|
+
return last !== undefined && kindOf(last) === "String" ? String(bodyOf(last).sval) : "?column?";
|
|
3758
|
+
}
|
|
3759
|
+
case "A_Indirection": {
|
|
3760
|
+
const ind = (b.indirection ?? []);
|
|
3761
|
+
for (let i = ind.length - 1; i >= 0; i--) {
|
|
3762
|
+
if (kindOf(ind[i]) === "String")
|
|
3763
|
+
return String(bodyOf(ind[i]).sval);
|
|
3764
|
+
if (kindOf(ind[i]) === "A_Star")
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
return isNode(b.arg) ? figureColname(b.arg) : "?column?";
|
|
3768
|
+
}
|
|
3769
|
+
case "FuncCall": return lastName(b.funcname);
|
|
3770
|
+
case "TypeCast": {
|
|
3771
|
+
const inner = isNode(b.arg) ? figureColname(b.arg) : "?column?";
|
|
3772
|
+
return inner !== "?column?" ? inner : lastName(b.typeName?.names);
|
|
3773
|
+
}
|
|
3774
|
+
case "CollateClause": return isNode(b.arg) ? figureColname(b.arg) : "?column?";
|
|
3775
|
+
case "GroupingFunc": return "grouping";
|
|
3776
|
+
case "SubLink": {
|
|
3777
|
+
if (b.subLinkType === "EXISTS_SUBLINK")
|
|
3778
|
+
return "exists";
|
|
3779
|
+
if (b.subLinkType === "ARRAY_SUBLINK")
|
|
3780
|
+
return "array";
|
|
3781
|
+
if (b.subLinkType !== "EXPR_SUBLINK" || !isNode(b.subselect))
|
|
3782
|
+
return "?column?";
|
|
3783
|
+
const first = (bodyOf(b.subselect).targetList ?? [])[0];
|
|
3784
|
+
return first !== undefined ? (bodyOf(first).name ?? (isNode(bodyOf(first).val) ? figureColname(bodyOf(first).val) : "?column?")) : "?column?";
|
|
3785
|
+
}
|
|
3786
|
+
case "CaseExpr": return "case";
|
|
3787
|
+
case "A_ArrayExpr": return "array";
|
|
3788
|
+
case "RowExpr": return "row";
|
|
3789
|
+
case "CoalesceExpr": return "coalesce";
|
|
3790
|
+
case "MinMaxExpr": return b.op === "IS_GREATEST" ? "greatest" : "least";
|
|
3791
|
+
case "SQLValueFunction": return String(b.op ?? "").replace(/^SVFOP_/, "").replace(/_N$/, "").toLowerCase() || "?column?";
|
|
3792
|
+
case "XmlExpr": return String(b.op ?? "").replace(/^IS_/, "").toLowerCase().replace(/^document$/, "?column?") || "?column?";
|
|
3793
|
+
case "XmlSerialize": return "xmlserialize";
|
|
3794
|
+
case "JsonObjectConstructor": return "json_object";
|
|
3795
|
+
case "JsonArrayConstructor":
|
|
3796
|
+
case "JsonArrayQueryConstructor": return "json_array";
|
|
3797
|
+
case "JsonObjectAgg": return "json_objectagg";
|
|
3798
|
+
case "JsonArrayAgg": return "json_arrayagg";
|
|
3799
|
+
case "JsonParseExpr": return "json";
|
|
3800
|
+
case "JsonScalarExpr": return "json_scalar";
|
|
3801
|
+
case "JsonSerializeExpr": return "json_serialize";
|
|
3802
|
+
case "JsonFuncExpr": return String(b.op ?? "").replace(/^JSON_(.*)_OP$/, "json_$1").toLowerCase() || "?column?";
|
|
3803
|
+
default: return "?column?";
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
/** every select list's unnamed target named as Postgres names it (a star keeps its expansion) */
|
|
3807
|
+
function nameTargets(v) {
|
|
3808
|
+
if (Array.isArray(v)) {
|
|
3809
|
+
for (const x of v)
|
|
3810
|
+
nameTargets(x);
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
if (v === null || typeof v !== "object")
|
|
3814
|
+
return;
|
|
3815
|
+
const b = isNode(v) ? bodyOf(v) : v;
|
|
3816
|
+
if (isNode(v) && kindOf(v) === "SelectStmt" || !isNode(v) && Array.isArray(b.targetList)) {
|
|
3817
|
+
for (const t of (b.targetList ?? [])) {
|
|
3818
|
+
const r = bodyOf(t);
|
|
3819
|
+
if (r.name === undefined && isNode(r.val) && !hasStar(r.val))
|
|
3820
|
+
r.name = figureColname(r.val);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
for (const [k, x] of Object.entries(b))
|
|
3824
|
+
nameTargets(armOf(k, x) ?? x);
|
|
3825
|
+
}
|
|
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);
|
|
3828
|
+
const own = JSON.parse(JSON.stringify(stmt));
|
|
3829
|
+
volatileWindows(own);
|
|
3830
|
+
r.adopt(own);
|
|
3831
|
+
nameTargets(own);
|
|
3832
|
+
r.indexShapes(own);
|
|
3833
|
+
const exact = r.exactThrough(own, analysis.fields);
|
|
3834
|
+
const tree = guardFilters(flattenBool(r.walk(own)));
|
|
3835
|
+
return { tree, dropLast: r.excludeRank, exact, clock: r.clock, jsonReads: [...r.jsonReads.values()] };
|
|
3836
|
+
}
|
|
3837
|
+
/** the functions whose every call is a new value (Postgres's volatile ones Mason answers) */
|
|
3838
|
+
const VOLATILE = new Set(["random", "gen_random_uuid", "uuid_generate_v4", "clock_timestamp", "timeofday"]);
|
|
3839
|
+
/** A window ordered or partitioned by a volatile function (`row_number() OVER (ORDER BY random())`, a random row per
|
|
3840
|
+
* group) is refused by name: DuckDB refuses a volatile expression there, and one it takes as deterministic is computed
|
|
3841
|
+
* once for every row (one value: the order is the input's, never random) */
|
|
3842
|
+
function volatileWindows(v) {
|
|
3843
|
+
const calls = (x) => Array.isArray(x) ? x.some(calls) : x !== null && typeof x === "object"
|
|
3844
|
+
&& ((isNode(x) && kindOf(x) === "FuncCall" && VOLATILE.has(lastName(bodyOf(x).funcname))) || Object.values(isNode(x) ? bodyOf(x) : x).some(calls));
|
|
3845
|
+
const check = (w) => {
|
|
3846
|
+
if (calls(w.orderClause) || calls(w.partitionClause)) {
|
|
3847
|
+
throw new Unrewritable("a window ordered or partitioned by random() (or another volatile function) is not supported by Mason", "compute it in a subquery and order by its column: SELECT …, row_number() OVER (ORDER BY r) FROM (SELECT *, random() AS r FROM t) AS s", "volatile-window");
|
|
3848
|
+
}
|
|
3849
|
+
};
|
|
3850
|
+
const walk = (x) => {
|
|
3851
|
+
if (Array.isArray(x)) {
|
|
3852
|
+
x.forEach(walk);
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
if (x === null || typeof x !== "object")
|
|
3856
|
+
return;
|
|
3857
|
+
if (isNode(x)) {
|
|
3858
|
+
const b = bodyOf(x);
|
|
3859
|
+
if (kindOf(x) === "FuncCall" && b.over !== undefined && b.over !== null && typeof b.over === "object")
|
|
3860
|
+
check(b.over);
|
|
3861
|
+
if (kindOf(x) === "WindowDef")
|
|
3862
|
+
check(b);
|
|
3863
|
+
Object.values(b).forEach(walk);
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
Object.values(x).forEach(walk);
|
|
3867
|
+
};
|
|
3868
|
+
walk(v);
|
|
3869
|
+
}
|
|
3870
|
+
/** An AND inside an AND (an OR inside an OR) as one list: a rewrite may make one (a text BETWEEN is two comparisons
|
|
3871
|
+
* under the collation), the printer writes it without parentheses and the parser reads one flat list back. The same
|
|
3872
|
+
* statement, by associativity, in three-valued logic too */
|
|
3873
|
+
function flattenBool(v) {
|
|
3874
|
+
if (Array.isArray(v))
|
|
3875
|
+
return v.map(flattenBool);
|
|
3876
|
+
if (v === null || typeof v !== "object")
|
|
3877
|
+
return v;
|
|
3878
|
+
const out = Object.fromEntries(Object.entries(v).map(([k, x]) => [k, flattenBool(x)]));
|
|
3879
|
+
if (isNode(v) && kindOf(v) === "BoolExpr") {
|
|
3880
|
+
const b = bodyOf(out);
|
|
3881
|
+
if (b.boolop === "AND_EXPR" || b.boolop === "OR_EXPR") {
|
|
3882
|
+
b.args = (b.args ?? []).flatMap((a) => kindOf(a) === "BoolExpr" && bodyOf(a).boolop === b.boolop ? bodyOf(a).args : [a]);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
return out;
|
|
3886
|
+
}
|
|
3887
|
+
/** whether a tree holds a node of this kind */
|
|
3888
|
+
function holdsKind(v, kind) {
|
|
3889
|
+
if (Array.isArray(v))
|
|
3890
|
+
return v.some((x) => holdsKind(x, kind));
|
|
3891
|
+
if (v === null || typeof v !== "object")
|
|
3892
|
+
return false;
|
|
3893
|
+
if (isNode(v) && kindOf(v) === kind)
|
|
3894
|
+
return true;
|
|
3895
|
+
return Object.values(isNode(v) ? bodyOf(v) : v).some((x) => holdsKind(x, kind));
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* AN AGGREGATE'S FILTER GUARDS ITS ARGUMENTS: Postgres evaluates an aggregate's arguments and ORDER BY only on the rows
|
|
3899
|
+
* its FILTER keeps, DuckDB on every row before it filters, so `min(a / b) FILTER (WHERE b <> 0)` raised 22012 where
|
|
3900
|
+
* Postgres answers (the oracle statement of min / max of a quotient). Each argument that reads a column
|
|
3901
|
+
* and is more than the column is read under a CASE of the filter, which DuckDB evaluates only where it holds. A filter
|
|
3902
|
+
* holding a subquery is left as it is (it would run twice), and so is a constant argument (percentile_cont's fraction
|
|
3903
|
+
* must stay one). So is a DISTINCT aggregate: DuckDB holds its ORDER BY to its arguments (42803), and a guarded
|
|
3904
|
+
* collation key no longer matched them (`string_agg(DISTINCT t, ', ') FILTER (…)`, the rerun at cc380e8d) — its
|
|
3905
|
+
* arguments may still raise on a row the filter drops.
|
|
3906
|
+
*/
|
|
3907
|
+
function guardFilters(v) {
|
|
3908
|
+
if (Array.isArray(v))
|
|
3909
|
+
return v.map(guardFilters);
|
|
3910
|
+
if (v === null || typeof v !== "object")
|
|
3911
|
+
return v;
|
|
3912
|
+
const out = Object.fromEntries(Object.entries(v).map(([k, x]) => [k, guardFilters(x)]));
|
|
3913
|
+
if (!isNode(v) || kindOf(v) !== "FuncCall")
|
|
3914
|
+
return out;
|
|
3915
|
+
const b = bodyOf(out);
|
|
3916
|
+
const filter = b.agg_filter;
|
|
3917
|
+
if (!isNode(filter) || holdsKind(filter, "SubLink") || b.agg_distinct === true)
|
|
3918
|
+
return out;
|
|
3919
|
+
const guard = (a) => kindOf(a) === "ColumnRef" || !holdsKind(a, "ColumnRef") ? a : caseWhen([[structuredClone(filter), a]], null);
|
|
3920
|
+
if (Array.isArray(b.args))
|
|
3921
|
+
b.args = b.args.map(guard);
|
|
3922
|
+
if (Array.isArray(b.agg_order))
|
|
3923
|
+
for (const sort of b.agg_order) {
|
|
3924
|
+
const sb = bodyOf(sort);
|
|
3925
|
+
if (isNode(sb.node))
|
|
3926
|
+
sb.node = guard(sb.node);
|
|
3927
|
+
}
|
|
3928
|
+
return out;
|
|
3929
|
+
}
|
|
3930
|
+
const isTypeName = (v) => v !== null && typeof v === "object" && !Array.isArray(v) && Array.isArray(v.names) && "typemod" in v;
|
|
3931
|
+
/** a type's name as the parser spells it: `bigint` is `pg_catalog.int8` once read */
|
|
3932
|
+
const TYPE_ALIAS = { bigint: "int8", integer: "int4", int: "int4", smallint: "int2", real: "float4", double: "float8", "double precision": "float8", boolean: "bool", decimal: "numeric", varchar: "varchar", "character varying": "varchar" };
|
|
3933
|
+
const canonicalType = (n) => { const last = (n[n.length - 1] ?? "").toLowerCase(); return TYPE_ALIAS[last] ?? last; };
|
|
3934
|
+
/**
|
|
3935
|
+
* THE MASON FUNCTIONS A REWRITTEN TREE CALLS: every `FuncCall` of a `__mason_*` name in it (`fn` builds each, the
|
|
3936
|
+
* stored path's `__mason_json_get` behind its CASE included), sorted, each once. What the plan carries to the executor
|
|
3937
|
+
* (src/duck/plan.ts PlannedSql `functions`), which cancels a statement that calls one through its session and
|
|
3938
|
+
* interrupts one that calls none (src/duck/store.ts "HOW A STATEMENT IS CANCELLED"). Read off the TREE, never the
|
|
3939
|
+
* printed text: a `__mason_` in the text is also a stored path's column or the view of a copy's visible columns
|
|
3940
|
+
* (src/duck/kept-route.ts), which call nothing — read as a call, every `SELECT *` over a copy with a stored path could
|
|
3941
|
+
* no longer be timed out or cancelled (a review finding).
|
|
3942
|
+
*/
|
|
3943
|
+
export function masonCalls(tree) {
|
|
3944
|
+
const found = new Set();
|
|
3945
|
+
const walk = (n) => {
|
|
3946
|
+
if (Array.isArray(n)) {
|
|
3947
|
+
for (const x of n)
|
|
3948
|
+
walk(x);
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3951
|
+
if (n === null || typeof n !== "object")
|
|
3952
|
+
return;
|
|
3953
|
+
for (const [k, v] of Object.entries(n)) {
|
|
3954
|
+
if (k === "FuncCall" && v !== null && typeof v === "object") {
|
|
3955
|
+
const name = lastName(v.funcname);
|
|
3956
|
+
if (name.startsWith("__mason_"))
|
|
3957
|
+
found.add(name);
|
|
3958
|
+
}
|
|
3959
|
+
walk(v);
|
|
3960
|
+
}
|
|
3961
|
+
};
|
|
3962
|
+
walk(tree);
|
|
3963
|
+
return [...found].sort();
|
|
3964
|
+
}
|
|
3965
|
+
/**
|
|
3966
|
+
* The printed statement READ BACK holds the rewritten tree: every field the tree has, equal (offsets aside), where the
|
|
3967
|
+
* parser may add the defaults a built node leaves out (a `location`, `typemod: -1`, `inh: true`). A field the printer
|
|
3968
|
+
* dropped or changed (a `WITH TIES`, an operator) is a difference.
|
|
3969
|
+
*/
|
|
3970
|
+
export function sameTree(back, tree) { return treeDifference(back, tree) === null; }
|
|
3971
|
+
/** where the read-back tree first differs from the rewritten one (a path and both values), or null */
|
|
3972
|
+
export function treeDifference(back, tree, at = "$") {
|
|
3973
|
+
const show = (v) => JSON.stringify(v)?.slice(0, 120) ?? String(v);
|
|
3974
|
+
if (isTypeName(tree) && isTypeName(back)) {
|
|
3975
|
+
const { names: _a, ...restTree } = tree, { names: _b, ...restBack } = back;
|
|
3976
|
+
const [t, b] = [canonicalType(names(tree.names)), canonicalType(names(back.names))];
|
|
3977
|
+
return t !== b ? `${at}.names: ${b} where the rewrite has ${t}` : treeDifference(restBack, restTree, at);
|
|
3978
|
+
}
|
|
3979
|
+
if (Array.isArray(tree)) {
|
|
3980
|
+
if (!Array.isArray(back) || back.length !== tree.length)
|
|
3981
|
+
return `${at}: ${show(back)} where the rewrite has ${show(tree)}`;
|
|
3982
|
+
for (let i = 0; i < tree.length; i++) {
|
|
3983
|
+
const d = treeDifference(back[i], tree[i], `${at}[${i}]`);
|
|
3984
|
+
if (d !== null)
|
|
3985
|
+
return d;
|
|
3986
|
+
}
|
|
3987
|
+
return null;
|
|
3988
|
+
}
|
|
3989
|
+
if (tree === null || typeof tree !== "object")
|
|
3990
|
+
return back === tree || (typeof tree === "number" && Number(back) === tree) ? null : `${at}: ${show(back)} where the rewrite has ${show(tree)}`;
|
|
3991
|
+
if (back === null || typeof back !== "object" || Array.isArray(back))
|
|
3992
|
+
return `${at}: ${show(back)} where the rewrite has ${show(tree)}`;
|
|
3993
|
+
for (const [k, v] of Object.entries(tree)) {
|
|
3994
|
+
if (k === "location" || k === "stmt_location" || k === "stmt_len" || /(^|_)list_(start|end)$/.test(k))
|
|
3995
|
+
continue;
|
|
3996
|
+
const d = treeDifference(back[k], v, `${at}.${k}`);
|
|
3997
|
+
if (d !== null)
|
|
3998
|
+
return d;
|
|
3999
|
+
}
|
|
4000
|
+
return null;
|
|
4001
|
+
}
|