@query-doctor/core 0.4.1 → 0.4.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/dist/index.cjs +1678 -2000
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +750 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +750 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1676 -24978
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -11
- package/dist/explain/rewriter.d.ts +0 -4
- package/dist/explain/rewriter.d.ts.map +0 -1
- package/dist/explain/traverse.d.ts +0 -3
- package/dist/explain/traverse.d.ts.map +0 -1
- package/dist/explain/tree.d.ts +0 -73
- package/dist/explain/tree.d.ts.map +0 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2352
- package/dist/index.js.map +0 -1
- package/dist/optimizer/genalgo.d.ts +0 -104
- package/dist/optimizer/genalgo.d.ts.map +0 -1
- package/dist/optimizer/genalgo.test.d.ts +0 -2
- package/dist/optimizer/genalgo.test.d.ts.map +0 -1
- package/dist/optimizer/index-candidate.d.ts +0 -23
- package/dist/optimizer/index-candidate.d.ts.map +0 -1
- package/dist/optimizer/index-shrinker.d.ts +0 -10
- package/dist/optimizer/index-shrinker.d.ts.map +0 -1
- package/dist/optimizer/index-shrinker.test.d.ts +0 -2
- package/dist/optimizer/index-shrinker.test.d.ts.map +0 -1
- package/dist/optimizer/index-tester.d.ts +0 -2
- package/dist/optimizer/index-tester.d.ts.map +0 -1
- package/dist/optimizer/pss-rewriter.d.ts +0 -11
- package/dist/optimizer/pss-rewriter.d.ts.map +0 -1
- package/dist/optimizer/pss-rewriter.test.d.ts +0 -2
- package/dist/optimizer/pss-rewriter.test.d.ts.map +0 -1
- package/dist/optimizer/statistics.d.ts +0 -360
- package/dist/optimizer/statistics.d.ts.map +0 -1
- package/dist/sql/analyzer.d.ts +0 -105
- package/dist/sql/analyzer.d.ts.map +0 -1
- package/dist/sql/analyzer.test.d.ts +0 -2
- package/dist/sql/analyzer.test.d.ts.map +0 -1
- package/dist/sql/builder.d.ts +0 -31
- package/dist/sql/builder.d.ts.map +0 -1
- package/dist/sql/builder.test.d.ts +0 -2
- package/dist/sql/builder.test.d.ts.map +0 -1
- package/dist/sql/database.d.ts +0 -84
- package/dist/sql/database.d.ts.map +0 -1
- package/dist/sql/indexes.d.ts +0 -8
- package/dist/sql/indexes.d.ts.map +0 -1
- package/dist/sql/nudges.d.ts +0 -16
- package/dist/sql/nudges.d.ts.map +0 -1
- package/dist/sql/permutations.d.ts +0 -10
- package/dist/sql/permutations.d.ts.map +0 -1
- package/dist/sql/permutations.test.d.ts +0 -2
- package/dist/sql/permutations.test.d.ts.map +0 -1
- package/dist/sql/pg-identifier.d.ts +0 -26
- package/dist/sql/pg-identifier.d.ts.map +0 -1
- package/dist/sql/pg-identifier.test.d.ts +0 -2
- package/dist/sql/pg-identifier.test.d.ts.map +0 -1
- package/dist/sql/walker.d.ts +0 -46
- package/dist/sql/walker.d.ts.map +0 -1
- package/dist/sql/walker.test.d.ts +0 -2
- package/dist/sql/walker.test.d.ts.map +0 -1
package/dist/index.js
DELETED
|
@@ -1,2352 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
-
|
|
6
|
-
// src/sql/analyzer.ts
|
|
7
|
-
import {
|
|
8
|
-
bgMagentaBright,
|
|
9
|
-
blue,
|
|
10
|
-
dim,
|
|
11
|
-
strikethrough
|
|
12
|
-
} from "colorette";
|
|
13
|
-
|
|
14
|
-
// src/sql/walker.ts
|
|
15
|
-
import { deparseSync } from "pgsql-deparser";
|
|
16
|
-
|
|
17
|
-
// src/sql/nudges.ts
|
|
18
|
-
function is(node, kind) {
|
|
19
|
-
return kind in node;
|
|
20
|
-
}
|
|
21
|
-
function isANode(node) {
|
|
22
|
-
if (typeof node !== "object" || node === null) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
const keys = Object.keys(node);
|
|
26
|
-
return keys.length === 1 && /^[A-Z]/.test(keys[0]);
|
|
27
|
-
}
|
|
28
|
-
function parseNudges(node, stack) {
|
|
29
|
-
const nudges = [];
|
|
30
|
-
if (is(node, "SelectStmt")) {
|
|
31
|
-
const star = node.SelectStmt.targetList?.find(
|
|
32
|
-
(target) => {
|
|
33
|
-
if (!(is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "ColumnRef"))) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
return target.ResTarget.val.ColumnRef.fields?.some(
|
|
37
|
-
(field) => is(field, "A_Star")
|
|
38
|
-
) ?? false;
|
|
39
|
-
}
|
|
40
|
-
);
|
|
41
|
-
if (star) {
|
|
42
|
-
nudges.push({
|
|
43
|
-
kind: "AVOID_SELECT_STAR",
|
|
44
|
-
severity: "INFO",
|
|
45
|
-
message: "Avoid using SELECT *",
|
|
46
|
-
location: star.ResTarget.location
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (is(node, "FuncCall")) {
|
|
51
|
-
const inWhereClause = stack.some((item) => item === "whereClause");
|
|
52
|
-
if (inWhereClause && node.FuncCall.args) {
|
|
53
|
-
const hasColumnRef = containsColumnRef(node.FuncCall.args);
|
|
54
|
-
if (hasColumnRef) {
|
|
55
|
-
nudges.push({
|
|
56
|
-
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
57
|
-
severity: "WARNING",
|
|
58
|
-
message: "Avoid using functions on columns in WHERE clause",
|
|
59
|
-
location: node.FuncCall.location
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (is(node, "SelectStmt")) {
|
|
65
|
-
const isSubquery = stack.some(
|
|
66
|
-
(item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr"
|
|
67
|
-
);
|
|
68
|
-
if (!isSubquery) {
|
|
69
|
-
const hasFromClause = node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;
|
|
70
|
-
if (hasFromClause) {
|
|
71
|
-
const hasActualTables = node.SelectStmt.fromClause.some((fromItem) => {
|
|
72
|
-
return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
|
|
73
|
-
});
|
|
74
|
-
if (hasActualTables) {
|
|
75
|
-
const firstTable = node.SelectStmt.fromClause.find(
|
|
76
|
-
(item) => is(item, "RangeVar")
|
|
77
|
-
);
|
|
78
|
-
const fromLocation = firstTable?.RangeVar.location;
|
|
79
|
-
if (!node.SelectStmt.whereClause) {
|
|
80
|
-
nudges.push({
|
|
81
|
-
kind: "MISSING_WHERE_CLAUSE",
|
|
82
|
-
severity: "INFO",
|
|
83
|
-
message: "Missing WHERE clause",
|
|
84
|
-
location: fromLocation
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
if (!node.SelectStmt.limitCount) {
|
|
88
|
-
nudges.push({
|
|
89
|
-
kind: "MISSING_LIMIT_CLAUSE",
|
|
90
|
-
severity: "INFO",
|
|
91
|
-
message: "Missing LIMIT clause",
|
|
92
|
-
location: fromLocation
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (is(node, "A_Expr")) {
|
|
100
|
-
const isEqualityOp = node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is(node.A_Expr.name[0], "String") && (node.A_Expr.name[0].String.sval === "=" || node.A_Expr.name[0].String.sval === "!=" || node.A_Expr.name[0].String.sval === "<>");
|
|
101
|
-
if (isEqualityOp) {
|
|
102
|
-
const leftIsNull = isNullConstant(node.A_Expr.lexpr);
|
|
103
|
-
const rightIsNull = isNullConstant(node.A_Expr.rexpr);
|
|
104
|
-
if (leftIsNull || rightIsNull) {
|
|
105
|
-
nudges.push({
|
|
106
|
-
kind: "USE_IS_NULL_NOT_EQUALS",
|
|
107
|
-
severity: "WARNING",
|
|
108
|
-
message: "Use IS NULL instead of = or != or <> for NULL comparisons",
|
|
109
|
-
location: node.A_Expr.location
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const isLikeOp = node.A_Expr.kind === "AEXPR_LIKE" || node.A_Expr.kind === "AEXPR_ILIKE";
|
|
114
|
-
if (isLikeOp && node.A_Expr.rexpr) {
|
|
115
|
-
const patternString = getStringConstantValue(node.A_Expr.rexpr);
|
|
116
|
-
if (patternString && patternString.startsWith("%")) {
|
|
117
|
-
let stringNode;
|
|
118
|
-
if (is(node.A_Expr.rexpr, "A_Const")) {
|
|
119
|
-
stringNode = node.A_Expr.rexpr.A_Const;
|
|
120
|
-
}
|
|
121
|
-
nudges.push({
|
|
122
|
-
kind: "AVOID_LEADING_WILDCARD_LIKE",
|
|
123
|
-
severity: "WARNING",
|
|
124
|
-
message: "Avoid using LIKE with leading wildcards",
|
|
125
|
-
location: stringNode?.location
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (is(node, "SelectStmt") && node.SelectStmt.distinctClause) {
|
|
131
|
-
nudges.push({
|
|
132
|
-
kind: "AVOID_DISTINCT_WITHOUT_REASON",
|
|
133
|
-
severity: "WARNING",
|
|
134
|
-
message: "Avoid using DISTINCT without a reason"
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
if (is(node, "JoinExpr")) {
|
|
138
|
-
if (!node.JoinExpr.quals) {
|
|
139
|
-
nudges.push({
|
|
140
|
-
kind: "MISSING_JOIN_CONDITION",
|
|
141
|
-
severity: "WARNING",
|
|
142
|
-
message: "Missing JOIN condition"
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
|
|
147
|
-
const tables = node.SelectStmt.fromClause.filter(
|
|
148
|
-
(item) => is(item, "RangeVar")
|
|
149
|
-
);
|
|
150
|
-
if (tables.length > 1) {
|
|
151
|
-
nudges.push({
|
|
152
|
-
kind: "MISSING_JOIN_CONDITION",
|
|
153
|
-
severity: "WARNING",
|
|
154
|
-
message: "Missing JOIN condition",
|
|
155
|
-
location: tables[1].RangeVar.location
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (is(node, "BoolExpr") && node.BoolExpr.boolop === "OR_EXPR") {
|
|
160
|
-
const orCount = countBoolOrConditions(node);
|
|
161
|
-
if (orCount >= 3) {
|
|
162
|
-
nudges.push({
|
|
163
|
-
kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
|
|
164
|
-
severity: "WARNING",
|
|
165
|
-
message: "Consider using IN instead of many ORs",
|
|
166
|
-
location: node.BoolExpr.location
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (is(node, "A_Expr")) {
|
|
171
|
-
if (node.A_Expr.kind === "AEXPR_IN") {
|
|
172
|
-
let list;
|
|
173
|
-
if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "List")) {
|
|
174
|
-
list = node.A_Expr.lexpr.List;
|
|
175
|
-
} else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "List")) {
|
|
176
|
-
list = node.A_Expr.rexpr.List;
|
|
177
|
-
}
|
|
178
|
-
if (list?.items && list.items.length >= 10) {
|
|
179
|
-
nudges.push({
|
|
180
|
-
kind: "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY",
|
|
181
|
-
message: "`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter",
|
|
182
|
-
severity: "INFO",
|
|
183
|
-
location: node.A_Expr.location
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return nudges;
|
|
189
|
-
}
|
|
190
|
-
function containsColumnRef(args) {
|
|
191
|
-
for (const arg of args) {
|
|
192
|
-
if (hasColumnRefInNode(arg)) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
function hasColumnRefInNode(node) {
|
|
199
|
-
if (isANode(node) && is(node, "ColumnRef")) {
|
|
200
|
-
return true;
|
|
201
|
-
}
|
|
202
|
-
if (typeof node !== "object" || node === null) {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
if (Array.isArray(node)) {
|
|
206
|
-
return node.some((item) => hasColumnRefInNode(item));
|
|
207
|
-
}
|
|
208
|
-
if (isANode(node)) {
|
|
209
|
-
const keys = Object.keys(node);
|
|
210
|
-
return hasColumnRefInNode(node[keys[0]]);
|
|
211
|
-
}
|
|
212
|
-
for (const child of Object.values(node)) {
|
|
213
|
-
if (hasColumnRefInNode(child)) {
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
function hasActualTablesInJoin(joinExpr) {
|
|
220
|
-
if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "RangeVar")) {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "JoinExpr")) {
|
|
224
|
-
if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "RangeVar")) {
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "JoinExpr")) {
|
|
232
|
-
if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
function isNullConstant(node) {
|
|
239
|
-
if (!node || typeof node !== "object") {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
if (isANode(node) && is(node, "A_Const")) {
|
|
243
|
-
return node.A_Const.isnull !== void 0;
|
|
244
|
-
}
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
function getStringConstantValue(node) {
|
|
248
|
-
if (!node || typeof node !== "object") {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
if (isANode(node) && is(node, "A_Const") && node.A_Const.sval) {
|
|
252
|
-
return node.A_Const.sval.sval || null;
|
|
253
|
-
}
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
function countBoolOrConditions(node) {
|
|
257
|
-
if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) {
|
|
258
|
-
return 1;
|
|
259
|
-
}
|
|
260
|
-
let count = 0;
|
|
261
|
-
for (const arg of node.BoolExpr.args) {
|
|
262
|
-
if (isANode(arg) && is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") {
|
|
263
|
-
count += countBoolOrConditions(arg);
|
|
264
|
-
} else {
|
|
265
|
-
count += 1;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return count;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// src/sql/walker.ts
|
|
272
|
-
var Walker = class _Walker {
|
|
273
|
-
constructor(query) {
|
|
274
|
-
this.query = query;
|
|
275
|
-
__publicField(this, "tableMappings", /* @__PURE__ */ new Map());
|
|
276
|
-
__publicField(this, "tempTables", /* @__PURE__ */ new Set());
|
|
277
|
-
__publicField(this, "highlights", []);
|
|
278
|
-
__publicField(this, "indexRepresentations", /* @__PURE__ */ new Set());
|
|
279
|
-
__publicField(this, "indexesToCheck", []);
|
|
280
|
-
__publicField(this, "highlightPositions", /* @__PURE__ */ new Set());
|
|
281
|
-
// used for tallying the amount of times we see stuff so
|
|
282
|
-
// we have a better idea of what to start off the algorithm with
|
|
283
|
-
__publicField(this, "seenReferences", /* @__PURE__ */ new Map());
|
|
284
|
-
__publicField(this, "shadowedAliases", []);
|
|
285
|
-
__publicField(this, "nudges", []);
|
|
286
|
-
}
|
|
287
|
-
walk(root) {
|
|
288
|
-
this.tableMappings = /* @__PURE__ */ new Map();
|
|
289
|
-
this.tempTables = /* @__PURE__ */ new Set();
|
|
290
|
-
this.highlights = [];
|
|
291
|
-
this.indexRepresentations = /* @__PURE__ */ new Set();
|
|
292
|
-
this.indexesToCheck = [];
|
|
293
|
-
this.highlightPositions = /* @__PURE__ */ new Set();
|
|
294
|
-
this.seenReferences = /* @__PURE__ */ new Map();
|
|
295
|
-
this.shadowedAliases = [];
|
|
296
|
-
this.nudges = [];
|
|
297
|
-
_Walker.traverse(root, (node, stack) => {
|
|
298
|
-
const nodeNudges = parseNudges(node, stack);
|
|
299
|
-
this.nudges = [...this.nudges, ...nodeNudges];
|
|
300
|
-
if (is2(node, "CommonTableExpr")) {
|
|
301
|
-
if (node.CommonTableExpr.ctename) {
|
|
302
|
-
this.tempTables.add(node.CommonTableExpr.ctename);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
if (is2(node, "RangeSubselect")) {
|
|
306
|
-
if (node.RangeSubselect.alias?.aliasname) {
|
|
307
|
-
this.tempTables.add(node.RangeSubselect.alias.aliasname);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (is2(node, "NullTest")) {
|
|
311
|
-
if (node.NullTest.arg && node.NullTest.nulltesttype && is2(node.NullTest.arg, "ColumnRef")) {
|
|
312
|
-
this.add(node.NullTest.arg, {
|
|
313
|
-
where: { nulltest: node.NullTest.nulltesttype }
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (is2(node, "RangeVar") && node.RangeVar.relname) {
|
|
318
|
-
const columnReference = {
|
|
319
|
-
text: node.RangeVar.relname,
|
|
320
|
-
start: node.RangeVar.location,
|
|
321
|
-
quoted: false
|
|
322
|
-
};
|
|
323
|
-
if (node.RangeVar.schemaname) {
|
|
324
|
-
columnReference.schema = node.RangeVar.schemaname;
|
|
325
|
-
}
|
|
326
|
-
this.tableMappings.set(node.RangeVar.relname, columnReference);
|
|
327
|
-
if (node.RangeVar.alias?.aliasname) {
|
|
328
|
-
const aliasName = node.RangeVar.alias.aliasname;
|
|
329
|
-
const existingMapping = this.tableMappings.get(aliasName);
|
|
330
|
-
const part = {
|
|
331
|
-
text: node.RangeVar.relname,
|
|
332
|
-
start: node.RangeVar.location,
|
|
333
|
-
// what goes here? the text here doesn't _really_ exist.
|
|
334
|
-
// so it can't be quoted or not quoted.
|
|
335
|
-
// Does it even matter?
|
|
336
|
-
quoted: true,
|
|
337
|
-
alias: aliasName
|
|
338
|
-
};
|
|
339
|
-
if (node.RangeVar.schemaname) {
|
|
340
|
-
part.schema = node.RangeVar.schemaname;
|
|
341
|
-
}
|
|
342
|
-
if (existingMapping) {
|
|
343
|
-
const isSystemCatalog = node.RangeVar.relname?.startsWith("pg_") ?? false;
|
|
344
|
-
if (!isSystemCatalog) {
|
|
345
|
-
console.warn(
|
|
346
|
-
`Ignoring alias ${aliasName} as it shadows an existing mapping for ${existingMapping.text}. We currently do not support alias shadowing.`
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
this.shadowedAliases.push(part);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
this.tableMappings.set(aliasName, part);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (is2(node, "SortBy")) {
|
|
356
|
-
if (node.SortBy.node && is2(node.SortBy.node, "ColumnRef")) {
|
|
357
|
-
this.add(node.SortBy.node, {
|
|
358
|
-
sort: {
|
|
359
|
-
dir: node.SortBy.sortby_dir ?? "SORTBY_DEFAULT",
|
|
360
|
-
nulls: node.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT"
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (is2(node, "JoinExpr") && node.JoinExpr.quals) {
|
|
366
|
-
if (is2(node.JoinExpr.quals, "A_Expr")) {
|
|
367
|
-
if (node.JoinExpr.quals.A_Expr.lexpr && is2(node.JoinExpr.quals.A_Expr.lexpr, "ColumnRef")) {
|
|
368
|
-
this.add(node.JoinExpr.quals.A_Expr.lexpr);
|
|
369
|
-
}
|
|
370
|
-
if (node.JoinExpr.quals.A_Expr.rexpr && is2(node.JoinExpr.quals.A_Expr.rexpr, "ColumnRef")) {
|
|
371
|
-
this.add(node.JoinExpr.quals.A_Expr.rexpr);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
if (is2(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
|
|
376
|
-
const opName = node.A_Expr.name?.[0] && is2(node.A_Expr.name[0], "String") && node.A_Expr.name[0].String.sval;
|
|
377
|
-
if (opName && (opName === "@>" || opName === "?" || opName === "?|" || opName === "?&")) {
|
|
378
|
-
const jsonbOperator = opName;
|
|
379
|
-
if (node.A_Expr.lexpr && is2(node.A_Expr.lexpr, "ColumnRef")) {
|
|
380
|
-
this.add(node.A_Expr.lexpr, { jsonbOperator });
|
|
381
|
-
}
|
|
382
|
-
if (node.A_Expr.rexpr && is2(node.A_Expr.rexpr, "ColumnRef")) {
|
|
383
|
-
this.add(node.A_Expr.rexpr, { jsonbOperator });
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (is2(node, "ColumnRef")) {
|
|
388
|
-
for (let i = 0; i < stack.length; i++) {
|
|
389
|
-
const inReturningList = stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef";
|
|
390
|
-
if (inReturningList) {
|
|
391
|
-
this.add(node, { ignored: true });
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
if (
|
|
395
|
-
// stack[i] === "SelectStmt" &&
|
|
396
|
-
stack[i + 1] === "targetList" && stack[i + 2] === "ResTarget" && stack[i + 3] === "val" && stack[i + 4] === "ColumnRef"
|
|
397
|
-
) {
|
|
398
|
-
this.add(node, { ignored: true });
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
if (stack[i] === "FuncCall" && stack[i + 1] === "args") {
|
|
402
|
-
this.add(node, { ignored: true });
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
this.add(node);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
return {
|
|
410
|
-
highlights: this.highlights,
|
|
411
|
-
indexRepresentations: this.indexRepresentations,
|
|
412
|
-
indexesToCheck: this.indexesToCheck,
|
|
413
|
-
shadowedAliases: this.shadowedAliases,
|
|
414
|
-
tempTables: this.tempTables,
|
|
415
|
-
tableMappings: this.tableMappings,
|
|
416
|
-
nudges: this.nudges
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
add(node, options) {
|
|
420
|
-
if (!node.ColumnRef.location) {
|
|
421
|
-
console.error(`Node did not have a location. Skipping`, node);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
if (!node.ColumnRef.fields) {
|
|
425
|
-
console.error(node);
|
|
426
|
-
throw new Error("Column reference must have fields");
|
|
427
|
-
}
|
|
428
|
-
let ignored = options?.ignored ?? false;
|
|
429
|
-
let runningLength = node.ColumnRef.location;
|
|
430
|
-
const parts = node.ColumnRef.fields.map(
|
|
431
|
-
(field, i, length) => {
|
|
432
|
-
if (!is2(field, "String") || !field.String.sval) {
|
|
433
|
-
const out = deparseSync(field);
|
|
434
|
-
ignored = true;
|
|
435
|
-
return {
|
|
436
|
-
quoted: out.startsWith('"'),
|
|
437
|
-
text: out,
|
|
438
|
-
start: runningLength
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
const start = runningLength;
|
|
442
|
-
const size = field.String.sval?.length ?? 0;
|
|
443
|
-
let quoted = false;
|
|
444
|
-
if (node.ColumnRef.location !== void 0) {
|
|
445
|
-
const boundary = this.query[runningLength];
|
|
446
|
-
if (boundary === '"') {
|
|
447
|
-
quoted = true;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
const isLastIteration = i === length.length - 1;
|
|
451
|
-
runningLength += size + (isLastIteration ? 0 : 1) + (quoted ? 2 : 0);
|
|
452
|
-
return {
|
|
453
|
-
text: field.String.sval,
|
|
454
|
-
start,
|
|
455
|
-
quoted
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
);
|
|
459
|
-
const end = runningLength;
|
|
460
|
-
if (this.highlightPositions.has(node.ColumnRef.location)) {
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
this.highlightPositions.add(node.ColumnRef.location);
|
|
464
|
-
const highlighted = `${this.query.slice(node.ColumnRef.location, end)}`;
|
|
465
|
-
const seen = this.seenReferences.get(highlighted);
|
|
466
|
-
if (!ignored) {
|
|
467
|
-
this.seenReferences.set(highlighted, (seen ?? 0) + 1);
|
|
468
|
-
}
|
|
469
|
-
const ref = {
|
|
470
|
-
frequency: seen ?? 1,
|
|
471
|
-
representation: highlighted,
|
|
472
|
-
parts,
|
|
473
|
-
ignored: ignored ?? false,
|
|
474
|
-
position: {
|
|
475
|
-
start: node.ColumnRef.location,
|
|
476
|
-
end
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
if (options?.sort) {
|
|
480
|
-
ref.sort = options.sort;
|
|
481
|
-
}
|
|
482
|
-
if (options?.where) {
|
|
483
|
-
ref.where = options.where;
|
|
484
|
-
}
|
|
485
|
-
if (options?.jsonbOperator) {
|
|
486
|
-
ref.jsonbOperator = options.jsonbOperator;
|
|
487
|
-
}
|
|
488
|
-
this.highlights.push(ref);
|
|
489
|
-
}
|
|
490
|
-
static traverse(node, callback) {
|
|
491
|
-
_Walker.doTraverse(node, [], callback);
|
|
492
|
-
}
|
|
493
|
-
static doTraverse(node, stack, callback) {
|
|
494
|
-
if (isANode2(node)) {
|
|
495
|
-
callback(node, [...stack, getNodeKind(node)]);
|
|
496
|
-
}
|
|
497
|
-
if (typeof node !== "object" || node === null) {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
if (Array.isArray(node)) {
|
|
501
|
-
for (const item of node) {
|
|
502
|
-
if (isANode2(item)) {
|
|
503
|
-
_Walker.doTraverse(item, stack, callback);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
} else if (isANode2(node)) {
|
|
507
|
-
const keys = Object.keys(node);
|
|
508
|
-
_Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);
|
|
509
|
-
} else {
|
|
510
|
-
for (const [key, child] of Object.entries(node)) {
|
|
511
|
-
_Walker.doTraverse(
|
|
512
|
-
child,
|
|
513
|
-
[...stack, key],
|
|
514
|
-
callback
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
function is2(node, kind) {
|
|
521
|
-
return kind in node;
|
|
522
|
-
}
|
|
523
|
-
function getNodeKind(node) {
|
|
524
|
-
const keys = Object.keys(node);
|
|
525
|
-
return keys[0];
|
|
526
|
-
}
|
|
527
|
-
function isANode2(node) {
|
|
528
|
-
if (typeof node !== "object" || node === null) {
|
|
529
|
-
return false;
|
|
530
|
-
}
|
|
531
|
-
const keys = Object.keys(node);
|
|
532
|
-
return keys.length === 1 && /^[A-Z]/.test(keys[0]);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// src/sql/analyzer.ts
|
|
536
|
-
var ignoredIdentifier = "__qd_placeholder";
|
|
537
|
-
var Analyzer = class {
|
|
538
|
-
constructor(parser) {
|
|
539
|
-
this.parser = parser;
|
|
540
|
-
}
|
|
541
|
-
async analyze(query, formattedQuery) {
|
|
542
|
-
const ast = await this.parser(query);
|
|
543
|
-
if (!ast.stmts) {
|
|
544
|
-
throw new Error(
|
|
545
|
-
"Query did not have any statements. This should probably never happen?"
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
const stmt = ast.stmts[0].stmt;
|
|
549
|
-
if (!stmt) {
|
|
550
|
-
throw new Error(
|
|
551
|
-
"Query did not have any statements. This should probably never happen?"
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
const walker = new Walker(query);
|
|
555
|
-
const {
|
|
556
|
-
highlights,
|
|
557
|
-
indexRepresentations,
|
|
558
|
-
indexesToCheck,
|
|
559
|
-
shadowedAliases,
|
|
560
|
-
tempTables,
|
|
561
|
-
tableMappings,
|
|
562
|
-
nudges
|
|
563
|
-
} = walker.walk(stmt);
|
|
564
|
-
const sortedHighlights = highlights.sort(
|
|
565
|
-
(a, b) => b.position.end - a.position.end
|
|
566
|
-
);
|
|
567
|
-
let currQuery = query;
|
|
568
|
-
for (const highlight of sortedHighlights) {
|
|
569
|
-
const parts = this.resolveTableAliases(highlight.parts, tableMappings);
|
|
570
|
-
if (parts.length === 0) {
|
|
571
|
-
console.error(highlight);
|
|
572
|
-
throw new Error("Highlight must have at least one part");
|
|
573
|
-
}
|
|
574
|
-
let color;
|
|
575
|
-
let skip = false;
|
|
576
|
-
if (highlight.ignored) {
|
|
577
|
-
color = (x) => dim(strikethrough(x));
|
|
578
|
-
skip = true;
|
|
579
|
-
} else if (parts.length === 2 && tempTables.has(parts[0].text) && // sometimes temp tables are aliased as existing tables
|
|
580
|
-
// we don't want to ignore them if they are
|
|
581
|
-
!tableMappings.has(parts[0].text)) {
|
|
582
|
-
color = blue;
|
|
583
|
-
skip = true;
|
|
584
|
-
} else {
|
|
585
|
-
color = bgMagentaBright;
|
|
586
|
-
}
|
|
587
|
-
const queryRepr = highlight.representation;
|
|
588
|
-
const queryBeforeMatch = currQuery.slice(0, highlight.position.start);
|
|
589
|
-
const queryAfterToken = currQuery.slice(highlight.position.end);
|
|
590
|
-
currQuery = `${queryBeforeMatch}${color(queryRepr)}${this.colorizeKeywords(
|
|
591
|
-
queryAfterToken,
|
|
592
|
-
color
|
|
593
|
-
)}`;
|
|
594
|
-
if (indexRepresentations.has(queryRepr)) {
|
|
595
|
-
skip = true;
|
|
596
|
-
}
|
|
597
|
-
if (!skip) {
|
|
598
|
-
indexesToCheck.push(highlight);
|
|
599
|
-
indexRepresentations.add(queryRepr);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
const referencedTables = [];
|
|
603
|
-
for (const value of tableMappings.values()) {
|
|
604
|
-
if (!value.alias) {
|
|
605
|
-
referencedTables.push({
|
|
606
|
-
schema: value.schema,
|
|
607
|
-
table: value.text
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
const { tags, queryWithoutTags } = this.extractSqlcommenter(query);
|
|
612
|
-
const formattedQueryWithoutTags = formattedQuery ? this.extractSqlcommenter(formattedQuery).queryWithoutTags : void 0;
|
|
613
|
-
return {
|
|
614
|
-
indexesToCheck,
|
|
615
|
-
ansiHighlightedQuery: currQuery,
|
|
616
|
-
referencedTables,
|
|
617
|
-
shadowedAliases,
|
|
618
|
-
tags,
|
|
619
|
-
queryWithoutTags,
|
|
620
|
-
formattedQueryWithoutTags,
|
|
621
|
-
nudges
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
deriveIndexes(tables, discovered, referencedTables) {
|
|
625
|
-
const allIndexes = [];
|
|
626
|
-
const seenIndexes = /* @__PURE__ */ new Set();
|
|
627
|
-
function addIndex(index) {
|
|
628
|
-
const key = `"${index.schema}":"${index.table}":"${index.column}"`;
|
|
629
|
-
if (seenIndexes.has(key)) {
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
seenIndexes.add(key);
|
|
633
|
-
allIndexes.push(index);
|
|
634
|
-
}
|
|
635
|
-
const matchingTables = this.filterReferences(referencedTables, tables);
|
|
636
|
-
for (const colReference of discovered) {
|
|
637
|
-
const partsCount = colReference.parts.length;
|
|
638
|
-
const columnOnlyReference = partsCount === 1;
|
|
639
|
-
const tableReference = partsCount === 2;
|
|
640
|
-
const fullReference = partsCount === 3;
|
|
641
|
-
if (columnOnlyReference) {
|
|
642
|
-
const [column] = colReference.parts;
|
|
643
|
-
const referencedColumn = this.normalize(column);
|
|
644
|
-
for (const table of matchingTables) {
|
|
645
|
-
if (!this.hasColumn(table, referencedColumn)) {
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
const index = {
|
|
649
|
-
schema: table.schemaName,
|
|
650
|
-
table: table.tableName,
|
|
651
|
-
column: referencedColumn
|
|
652
|
-
};
|
|
653
|
-
if (colReference.sort) {
|
|
654
|
-
index.sort = colReference.sort;
|
|
655
|
-
}
|
|
656
|
-
if (colReference.where) {
|
|
657
|
-
index.where = colReference.where;
|
|
658
|
-
}
|
|
659
|
-
if (colReference.jsonbOperator) {
|
|
660
|
-
index.jsonbOperator = colReference.jsonbOperator;
|
|
661
|
-
}
|
|
662
|
-
addIndex(index);
|
|
663
|
-
}
|
|
664
|
-
} else if (tableReference) {
|
|
665
|
-
const [table, column] = colReference.parts;
|
|
666
|
-
const referencedTable = this.normalize(table);
|
|
667
|
-
const referencedColumn = this.normalize(column);
|
|
668
|
-
for (const matchingTable of matchingTables) {
|
|
669
|
-
if (!this.hasColumn(matchingTable, referencedColumn)) {
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
const index = {
|
|
673
|
-
schema: matchingTable.schemaName,
|
|
674
|
-
table: referencedTable,
|
|
675
|
-
column: referencedColumn
|
|
676
|
-
};
|
|
677
|
-
if (colReference.sort) {
|
|
678
|
-
index.sort = colReference.sort;
|
|
679
|
-
}
|
|
680
|
-
if (colReference.where) {
|
|
681
|
-
index.where = colReference.where;
|
|
682
|
-
}
|
|
683
|
-
if (colReference.jsonbOperator) {
|
|
684
|
-
index.jsonbOperator = colReference.jsonbOperator;
|
|
685
|
-
}
|
|
686
|
-
addIndex(index);
|
|
687
|
-
}
|
|
688
|
-
} else if (fullReference) {
|
|
689
|
-
const [schema, table, column] = colReference.parts;
|
|
690
|
-
const referencedSchema = this.normalize(schema);
|
|
691
|
-
const referencedTable = this.normalize(table);
|
|
692
|
-
const referencedColumn = this.normalize(column);
|
|
693
|
-
const index = {
|
|
694
|
-
schema: referencedSchema,
|
|
695
|
-
table: referencedTable,
|
|
696
|
-
column: referencedColumn
|
|
697
|
-
};
|
|
698
|
-
if (colReference.sort) {
|
|
699
|
-
index.sort = colReference.sort;
|
|
700
|
-
}
|
|
701
|
-
if (colReference.where) {
|
|
702
|
-
index.where = colReference.where;
|
|
703
|
-
}
|
|
704
|
-
if (colReference.jsonbOperator) {
|
|
705
|
-
index.jsonbOperator = colReference.jsonbOperator;
|
|
706
|
-
}
|
|
707
|
-
addIndex(index);
|
|
708
|
-
} else {
|
|
709
|
-
console.error(
|
|
710
|
-
"Column reference has too many parts. The query is malformed",
|
|
711
|
-
colReference
|
|
712
|
-
);
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
return allIndexes;
|
|
717
|
-
}
|
|
718
|
-
filterReferences(referencedTables, tables) {
|
|
719
|
-
const matchingTables = [];
|
|
720
|
-
for (const referencedTable of referencedTables) {
|
|
721
|
-
const refs = tables.filter(({ tableName, schemaName }) => {
|
|
722
|
-
let schemaMatches = true;
|
|
723
|
-
if (referencedTable.schema) {
|
|
724
|
-
schemaMatches = schemaName === referencedTable.schema;
|
|
725
|
-
}
|
|
726
|
-
return schemaMatches && tableName === referencedTable.table;
|
|
727
|
-
});
|
|
728
|
-
matchingTables.push(...refs);
|
|
729
|
-
}
|
|
730
|
-
return matchingTables;
|
|
731
|
-
}
|
|
732
|
-
hasColumn(table, columnName) {
|
|
733
|
-
return table.columns?.some((column) => column.columnName === columnName) ?? false;
|
|
734
|
-
}
|
|
735
|
-
colorizeKeywords(query, color) {
|
|
736
|
-
return query.replace(
|
|
737
|
-
// eh? This kinda sucks
|
|
738
|
-
/(^\s+)(asc|desc)?(\s+(nulls first|nulls last))?/i,
|
|
739
|
-
(_, pre, dir, spaceNulls, nulls) => {
|
|
740
|
-
return `${pre}${dir ? color(dir) : ""}${nulls ? spaceNulls.replace(nulls, color(nulls)) : ""}`;
|
|
741
|
-
}
|
|
742
|
-
).replace(/(^\s+)(is (null|not null))/i, (_, pre, nulltest) => {
|
|
743
|
-
return `${pre}${color(nulltest)}`;
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Resolves aliases such as `a.b` to `x.b` if `a` is a known
|
|
748
|
-
* alias to a table called x.
|
|
749
|
-
*
|
|
750
|
-
* Ignores all other combination of parts such as `a.b.c`
|
|
751
|
-
*/
|
|
752
|
-
resolveTableAliases(parts, tableMappings) {
|
|
753
|
-
if (parts.length !== 2) {
|
|
754
|
-
return parts;
|
|
755
|
-
}
|
|
756
|
-
const tablePart = parts[0];
|
|
757
|
-
const mapping = tableMappings.get(tablePart.text);
|
|
758
|
-
if (mapping) {
|
|
759
|
-
parts[0] = mapping;
|
|
760
|
-
}
|
|
761
|
-
return parts;
|
|
762
|
-
}
|
|
763
|
-
normalize(columnReference) {
|
|
764
|
-
return columnReference.quoted ? columnReference.text : (
|
|
765
|
-
// postgres automatically lowercases column names if not quoted
|
|
766
|
-
columnReference.text.toLowerCase()
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
extractSqlcommenter(query) {
|
|
770
|
-
const trimmedQuery = query.trimEnd();
|
|
771
|
-
const startPosition = trimmedQuery.lastIndexOf("/*");
|
|
772
|
-
const endPosition = trimmedQuery.lastIndexOf("*/");
|
|
773
|
-
if (startPosition === -1 || endPosition === -1) {
|
|
774
|
-
return { tags: [], queryWithoutTags: trimmedQuery };
|
|
775
|
-
}
|
|
776
|
-
const queryWithoutTags = trimmedQuery.slice(0, startPosition);
|
|
777
|
-
const tagString = trimmedQuery.slice(startPosition + 2, endPosition).trim();
|
|
778
|
-
if (!tagString || typeof tagString !== "string") {
|
|
779
|
-
return { tags: [], queryWithoutTags };
|
|
780
|
-
}
|
|
781
|
-
const tags = [];
|
|
782
|
-
for (const match of tagString.split(",")) {
|
|
783
|
-
const [key, value] = match.split("=");
|
|
784
|
-
if (!key || !value) {
|
|
785
|
-
if (tags.length > 0) {
|
|
786
|
-
console.warn(
|
|
787
|
-
`Invalid sqlcommenter tag: ${match} in comment: ${tagString}. Ignoring`
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
continue;
|
|
791
|
-
}
|
|
792
|
-
try {
|
|
793
|
-
let sliceStart = 0;
|
|
794
|
-
if (value.startsWith("'")) {
|
|
795
|
-
sliceStart = 1;
|
|
796
|
-
}
|
|
797
|
-
let sliceEnd = value.length;
|
|
798
|
-
if (value.endsWith("'")) {
|
|
799
|
-
sliceEnd -= 1;
|
|
800
|
-
}
|
|
801
|
-
const decoded = decodeURIComponent(value.slice(sliceStart, sliceEnd));
|
|
802
|
-
tags.push({ key: key.trim(), value: decoded });
|
|
803
|
-
} catch (err) {
|
|
804
|
-
console.error(err);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
return { tags, queryWithoutTags };
|
|
808
|
-
}
|
|
809
|
-
};
|
|
810
|
-
|
|
811
|
-
// src/sql/database.ts
|
|
812
|
-
import { z } from "zod";
|
|
813
|
-
var PostgresVersion = z.string().brand("PostgresVersion");
|
|
814
|
-
async function dropIndex(tx, index) {
|
|
815
|
-
try {
|
|
816
|
-
await tx.exec(`
|
|
817
|
-
savepoint idx_drop;
|
|
818
|
-
drop index if exists ${index} cascade;
|
|
819
|
-
`);
|
|
820
|
-
return true;
|
|
821
|
-
} catch (error) {
|
|
822
|
-
await tx.exec(`rollback to idx_drop`);
|
|
823
|
-
return false;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// src/sql/indexes.ts
|
|
828
|
-
function isIndexSupported(index) {
|
|
829
|
-
return index.index_type === "btree" || index.index_type === "gin";
|
|
830
|
-
}
|
|
831
|
-
function isIndexProbablyDroppable(index) {
|
|
832
|
-
return !index.is_primary && !index.is_unique;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// src/sql/builder.ts
|
|
836
|
-
var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
837
|
-
constructor(query) {
|
|
838
|
-
this.query = query;
|
|
839
|
-
__publicField(this, "commands", {});
|
|
840
|
-
__publicField(this, "isIntrospection", false);
|
|
841
|
-
__publicField(this, "explainFlags", []);
|
|
842
|
-
__publicField(this, "_preamble", 0);
|
|
843
|
-
__publicField(this, "parameters", {});
|
|
844
|
-
// substitution for `limit $1` -> `limit 5`
|
|
845
|
-
__publicField(this, "limitSubstitution");
|
|
846
|
-
}
|
|
847
|
-
get preamble() {
|
|
848
|
-
return this._preamble;
|
|
849
|
-
}
|
|
850
|
-
static createIndex(definition, name) {
|
|
851
|
-
if (name) {
|
|
852
|
-
return new _PostgresQueryBuilder(
|
|
853
|
-
`create index "${name}" on ${definition};`
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
return new _PostgresQueryBuilder(`create index on ${definition};`);
|
|
857
|
-
}
|
|
858
|
-
enable(command, value = true) {
|
|
859
|
-
const commandString = `enable_${command}`;
|
|
860
|
-
if (value) {
|
|
861
|
-
this.commands[commandString] = "on";
|
|
862
|
-
} else {
|
|
863
|
-
this.commands[commandString] = "off";
|
|
864
|
-
}
|
|
865
|
-
return this;
|
|
866
|
-
}
|
|
867
|
-
withQuery(query) {
|
|
868
|
-
this.query = query;
|
|
869
|
-
return this;
|
|
870
|
-
}
|
|
871
|
-
introspect() {
|
|
872
|
-
this.isIntrospection = true;
|
|
873
|
-
return this;
|
|
874
|
-
}
|
|
875
|
-
explain(flags) {
|
|
876
|
-
this.explainFlags = flags;
|
|
877
|
-
return this;
|
|
878
|
-
}
|
|
879
|
-
parameterize(parameters) {
|
|
880
|
-
Object.assign(this.parameters, parameters);
|
|
881
|
-
return this;
|
|
882
|
-
}
|
|
883
|
-
replaceLimit(limit) {
|
|
884
|
-
this.limitSubstitution = limit;
|
|
885
|
-
return this;
|
|
886
|
-
}
|
|
887
|
-
build() {
|
|
888
|
-
let commands = this.generateSetCommands();
|
|
889
|
-
commands += this.generateExplain().query;
|
|
890
|
-
if (this.isIntrospection) {
|
|
891
|
-
commands += " -- @qd_introspection";
|
|
892
|
-
}
|
|
893
|
-
return commands;
|
|
894
|
-
}
|
|
895
|
-
/** Return the "set a=b" parts of the command in the query separate from the explain select ... part */
|
|
896
|
-
buildParts() {
|
|
897
|
-
const commands = this.generateSetCommands();
|
|
898
|
-
const explain = this.generateExplain();
|
|
899
|
-
this._preamble = explain.preamble;
|
|
900
|
-
if (this.isIntrospection) {
|
|
901
|
-
explain.query += " -- @qd_introspection";
|
|
902
|
-
}
|
|
903
|
-
return { commands, query: explain.query };
|
|
904
|
-
}
|
|
905
|
-
generateSetCommands() {
|
|
906
|
-
let commands = "";
|
|
907
|
-
for (const key in this.commands) {
|
|
908
|
-
const value = this.commands[key];
|
|
909
|
-
commands += `set local ${key}=${value};
|
|
910
|
-
`;
|
|
911
|
-
}
|
|
912
|
-
return commands;
|
|
913
|
-
}
|
|
914
|
-
generateExplain() {
|
|
915
|
-
let finalQuery = "";
|
|
916
|
-
if (this.explainFlags.length > 0) {
|
|
917
|
-
finalQuery += `explain (${this.explainFlags.join(", ")}) `;
|
|
918
|
-
}
|
|
919
|
-
const query = this.substituteQuery();
|
|
920
|
-
const semicolon = query.endsWith(";") ? "" : ";";
|
|
921
|
-
const preamble = finalQuery.length;
|
|
922
|
-
finalQuery += `${query}${semicolon}`;
|
|
923
|
-
return { query: finalQuery, preamble };
|
|
924
|
-
}
|
|
925
|
-
substituteQuery() {
|
|
926
|
-
let query = this.query;
|
|
927
|
-
if (this.limitSubstitution !== void 0) {
|
|
928
|
-
query = query.replace(
|
|
929
|
-
/limit\s+\$\d+/g,
|
|
930
|
-
`limit ${this.limitSubstitution}`
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
for (const [key, value] of Object.entries(this.parameters)) {
|
|
934
|
-
query = query.replaceAll(`\\$${key}`, value.toString());
|
|
935
|
-
}
|
|
936
|
-
return query;
|
|
937
|
-
}
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
// src/sql/pg-identifier.ts
|
|
941
|
-
var _PgIdentifier = class _PgIdentifier {
|
|
942
|
-
constructor(value, quoted) {
|
|
943
|
-
this.value = value;
|
|
944
|
-
this.quoted = quoted;
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Constructs an identifier from a single part (column or table name).
|
|
948
|
-
* When quoting identifiers like `select table.col` use {@link fromParts} instead
|
|
949
|
-
*/
|
|
950
|
-
static fromString(identifier) {
|
|
951
|
-
const identifierRegex = /^[a-z_][a-zA-Z0-9_]*$/;
|
|
952
|
-
const match = identifier.match(/^"(.+)"$/);
|
|
953
|
-
if (match) {
|
|
954
|
-
const value = match[1];
|
|
955
|
-
const quoted2 = !identifierRegex.test(value) || this.reservedKeywords.has(value.toLowerCase());
|
|
956
|
-
return new _PgIdentifier(value, quoted2);
|
|
957
|
-
}
|
|
958
|
-
const quoted = !identifierRegex.test(identifier) || this.reservedKeywords.has(identifier.toLowerCase());
|
|
959
|
-
return new _PgIdentifier(identifier, quoted);
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Quotes parts of an identifier like `select schema.table.col`.
|
|
963
|
-
* A separate function is necessary because postgres will treat
|
|
964
|
-
* `select "HELLO.WORLD"` as a column name. It has to be like
|
|
965
|
-
* `select "HELLO"."WORLD"` instead.
|
|
966
|
-
*/
|
|
967
|
-
static fromParts(...identifiers) {
|
|
968
|
-
return new _PgIdentifier(
|
|
969
|
-
identifiers.map((identifier) => {
|
|
970
|
-
if (typeof identifier === "string") {
|
|
971
|
-
return _PgIdentifier.fromString(identifier);
|
|
972
|
-
} else {
|
|
973
|
-
return identifier;
|
|
974
|
-
}
|
|
975
|
-
}).join("."),
|
|
976
|
-
false
|
|
977
|
-
);
|
|
978
|
-
}
|
|
979
|
-
toString() {
|
|
980
|
-
if (this.quoted) {
|
|
981
|
-
return `"${this.value.replace(/"/g, '""')}"`;
|
|
982
|
-
}
|
|
983
|
-
return this.value;
|
|
984
|
-
}
|
|
985
|
-
toJSON() {
|
|
986
|
-
return this.toString();
|
|
987
|
-
}
|
|
988
|
-
};
|
|
989
|
-
// Every keyword that's not explicitly marked as
|
|
990
|
-
// unreserved in src/include/parser/kwlist.h
|
|
991
|
-
__publicField(_PgIdentifier, "reservedKeywords", /* @__PURE__ */ new Set([
|
|
992
|
-
"all",
|
|
993
|
-
"analyse",
|
|
994
|
-
"analyze",
|
|
995
|
-
"and",
|
|
996
|
-
"any",
|
|
997
|
-
"array",
|
|
998
|
-
"as",
|
|
999
|
-
"asc",
|
|
1000
|
-
"asymmetric",
|
|
1001
|
-
"authorization",
|
|
1002
|
-
"between",
|
|
1003
|
-
"bigint",
|
|
1004
|
-
"binary",
|
|
1005
|
-
"bit",
|
|
1006
|
-
"boolean",
|
|
1007
|
-
"both",
|
|
1008
|
-
"case",
|
|
1009
|
-
"cast",
|
|
1010
|
-
"char",
|
|
1011
|
-
"character",
|
|
1012
|
-
"check",
|
|
1013
|
-
"coalesce",
|
|
1014
|
-
"collate",
|
|
1015
|
-
"collation",
|
|
1016
|
-
"column",
|
|
1017
|
-
"concurrently",
|
|
1018
|
-
"constraint",
|
|
1019
|
-
"create",
|
|
1020
|
-
"cross",
|
|
1021
|
-
"current_catalog",
|
|
1022
|
-
"current_date",
|
|
1023
|
-
"current_role",
|
|
1024
|
-
"current_schema",
|
|
1025
|
-
"current_time",
|
|
1026
|
-
"current_timestamp",
|
|
1027
|
-
"current_user",
|
|
1028
|
-
"dec",
|
|
1029
|
-
"decimal",
|
|
1030
|
-
"default",
|
|
1031
|
-
"deferrable",
|
|
1032
|
-
"desc",
|
|
1033
|
-
"distinct",
|
|
1034
|
-
"do",
|
|
1035
|
-
"else",
|
|
1036
|
-
"end",
|
|
1037
|
-
"except",
|
|
1038
|
-
"exists",
|
|
1039
|
-
"extract",
|
|
1040
|
-
"false",
|
|
1041
|
-
"fetch",
|
|
1042
|
-
"float",
|
|
1043
|
-
"for",
|
|
1044
|
-
"foreign",
|
|
1045
|
-
"freeze",
|
|
1046
|
-
"from",
|
|
1047
|
-
"full",
|
|
1048
|
-
"grant",
|
|
1049
|
-
"greatest",
|
|
1050
|
-
"group",
|
|
1051
|
-
"grouping",
|
|
1052
|
-
"having",
|
|
1053
|
-
"ilike",
|
|
1054
|
-
"in",
|
|
1055
|
-
"initially",
|
|
1056
|
-
"inner",
|
|
1057
|
-
"inout",
|
|
1058
|
-
"int",
|
|
1059
|
-
"integer",
|
|
1060
|
-
"intersect",
|
|
1061
|
-
"interval",
|
|
1062
|
-
"into",
|
|
1063
|
-
"is",
|
|
1064
|
-
"isnull",
|
|
1065
|
-
"join",
|
|
1066
|
-
"json",
|
|
1067
|
-
"json_array",
|
|
1068
|
-
"json_arrayagg",
|
|
1069
|
-
"json_exists",
|
|
1070
|
-
"json_object",
|
|
1071
|
-
"json_objectagg",
|
|
1072
|
-
"json_query",
|
|
1073
|
-
"json_scalar",
|
|
1074
|
-
"json_serialize",
|
|
1075
|
-
"json_table",
|
|
1076
|
-
"json_value",
|
|
1077
|
-
"lateral",
|
|
1078
|
-
"leading",
|
|
1079
|
-
"least",
|
|
1080
|
-
"left",
|
|
1081
|
-
"like",
|
|
1082
|
-
"limit",
|
|
1083
|
-
"localtime",
|
|
1084
|
-
"localtimestamp",
|
|
1085
|
-
"merge_action",
|
|
1086
|
-
"national",
|
|
1087
|
-
"natural",
|
|
1088
|
-
"nchar",
|
|
1089
|
-
"none",
|
|
1090
|
-
"normalize",
|
|
1091
|
-
"not",
|
|
1092
|
-
"notnull",
|
|
1093
|
-
"null",
|
|
1094
|
-
"nullif",
|
|
1095
|
-
"numeric",
|
|
1096
|
-
"offset",
|
|
1097
|
-
"on",
|
|
1098
|
-
"only",
|
|
1099
|
-
"or",
|
|
1100
|
-
"order",
|
|
1101
|
-
"out",
|
|
1102
|
-
"outer",
|
|
1103
|
-
"overlaps",
|
|
1104
|
-
"overlay",
|
|
1105
|
-
"placing",
|
|
1106
|
-
"position",
|
|
1107
|
-
"precision",
|
|
1108
|
-
"primary",
|
|
1109
|
-
"real",
|
|
1110
|
-
"references",
|
|
1111
|
-
"returning",
|
|
1112
|
-
"right",
|
|
1113
|
-
"row",
|
|
1114
|
-
"select",
|
|
1115
|
-
"session_user",
|
|
1116
|
-
"setof",
|
|
1117
|
-
"similar",
|
|
1118
|
-
"smallint",
|
|
1119
|
-
"some",
|
|
1120
|
-
"substring",
|
|
1121
|
-
"symmetric",
|
|
1122
|
-
"system_user",
|
|
1123
|
-
"table",
|
|
1124
|
-
"tablesample",
|
|
1125
|
-
"then",
|
|
1126
|
-
"time",
|
|
1127
|
-
"timestamp",
|
|
1128
|
-
"to",
|
|
1129
|
-
"trailing",
|
|
1130
|
-
"treat",
|
|
1131
|
-
"trim",
|
|
1132
|
-
"true",
|
|
1133
|
-
"union",
|
|
1134
|
-
"unique",
|
|
1135
|
-
"user",
|
|
1136
|
-
"using",
|
|
1137
|
-
"values",
|
|
1138
|
-
"varchar",
|
|
1139
|
-
"variadic",
|
|
1140
|
-
"verbose",
|
|
1141
|
-
"when",
|
|
1142
|
-
"where",
|
|
1143
|
-
"window",
|
|
1144
|
-
"with",
|
|
1145
|
-
"xmlattributes",
|
|
1146
|
-
"xmlconcat",
|
|
1147
|
-
"xmlelement",
|
|
1148
|
-
"xmlexists",
|
|
1149
|
-
"xmlforest",
|
|
1150
|
-
"xmlnamespaces",
|
|
1151
|
-
"xmlparse",
|
|
1152
|
-
"xmlpi",
|
|
1153
|
-
"xmlroot",
|
|
1154
|
-
"xmlserialize",
|
|
1155
|
-
"xmltable"
|
|
1156
|
-
]));
|
|
1157
|
-
var PgIdentifier = _PgIdentifier;
|
|
1158
|
-
|
|
1159
|
-
// src/optimizer/genalgo.ts
|
|
1160
|
-
import { blue as blue2, gray, green, magenta, red, yellow } from "colorette";
|
|
1161
|
-
|
|
1162
|
-
// src/sql/permutations.ts
|
|
1163
|
-
function permutationsWithDescendingLength(arr) {
|
|
1164
|
-
const collected = [];
|
|
1165
|
-
function collect(path, rest) {
|
|
1166
|
-
for (let i = 0; i < rest.length; i++) {
|
|
1167
|
-
const nextRest = [...rest.slice(0, i), ...rest.slice(i + 1)];
|
|
1168
|
-
const nextPath = [...path, rest[i]];
|
|
1169
|
-
collected.push(nextPath);
|
|
1170
|
-
collect(nextPath, nextRest);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
collect([], arr);
|
|
1174
|
-
collected.sort((a, b) => b.length - a.length);
|
|
1175
|
-
return collected;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// src/optimizer/genalgo.ts
|
|
1179
|
-
var _IndexOptimizer = class _IndexOptimizer {
|
|
1180
|
-
constructor(db, statistics, existingIndexes, config = {}) {
|
|
1181
|
-
this.db = db;
|
|
1182
|
-
this.statistics = statistics;
|
|
1183
|
-
this.existingIndexes = existingIndexes;
|
|
1184
|
-
this.config = config;
|
|
1185
|
-
}
|
|
1186
|
-
async run(builder, indexes, beforeQuery) {
|
|
1187
|
-
const baseExplain = await this.testQueryWithStats(builder, async (tx) => {
|
|
1188
|
-
if (beforeQuery) {
|
|
1189
|
-
await beforeQuery(tx);
|
|
1190
|
-
}
|
|
1191
|
-
});
|
|
1192
|
-
const baseCost = Number(baseExplain.Plan["Total Cost"]);
|
|
1193
|
-
if (baseCost === 0) {
|
|
1194
|
-
return {
|
|
1195
|
-
kind: "zero_cost_plan",
|
|
1196
|
-
explainPlan: baseExplain.Plan
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
const toCreate = this.indexesToCreate(indexes);
|
|
1200
|
-
const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
|
|
1201
|
-
if (beforeQuery) {
|
|
1202
|
-
await beforeQuery(tx);
|
|
1203
|
-
}
|
|
1204
|
-
for (const permutation of toCreate) {
|
|
1205
|
-
const createIndex = PostgresQueryBuilder.createIndex(
|
|
1206
|
-
permutation.definition,
|
|
1207
|
-
permutation.name
|
|
1208
|
-
).introspect().build();
|
|
1209
|
-
await tx.exec(createIndex);
|
|
1210
|
-
}
|
|
1211
|
-
});
|
|
1212
|
-
const finalCost = Number(finalExplain.Plan["Total Cost"]);
|
|
1213
|
-
if (this.config.debug) {
|
|
1214
|
-
console.dir(finalExplain, { depth: null });
|
|
1215
|
-
}
|
|
1216
|
-
const deltaPercentage = (baseCost - finalCost) / baseCost * 100;
|
|
1217
|
-
if (finalCost < baseCost) {
|
|
1218
|
-
console.log(
|
|
1219
|
-
` \u{1F389}\u{1F389}\u{1F389} ${green(`+${deltaPercentage.toFixed(2).padStart(5, "0")}%`)}`
|
|
1220
|
-
);
|
|
1221
|
-
} else if (finalCost > baseCost) {
|
|
1222
|
-
console.log(
|
|
1223
|
-
`${red(
|
|
1224
|
-
`-${Math.abs(deltaPercentage).toFixed(2).padStart(5, "0")}%`
|
|
1225
|
-
)} ${gray("If there's a better index, we haven't tried it")}`
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
const baseIndexes = this.findUsedIndexes(baseExplain.Plan);
|
|
1229
|
-
const finalIndexes = this.findUsedIndexes(finalExplain.Plan);
|
|
1230
|
-
const triedIndexes = new Map(
|
|
1231
|
-
toCreate.map((index) => [index.name.toString(), index])
|
|
1232
|
-
);
|
|
1233
|
-
this.replaceUsedIndexesWithDefinition(finalExplain.Plan, triedIndexes);
|
|
1234
|
-
return {
|
|
1235
|
-
kind: "ok",
|
|
1236
|
-
baseCost,
|
|
1237
|
-
finalCost,
|
|
1238
|
-
newIndexes: finalIndexes.newIndexes,
|
|
1239
|
-
existingIndexes: baseIndexes.existingIndexes,
|
|
1240
|
-
triedIndexes,
|
|
1241
|
-
baseExplainPlan: baseExplain.Plan,
|
|
1242
|
-
explainPlan: finalExplain.Plan
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
async runWithoutIndexes(builder) {
|
|
1246
|
-
return await this.testQueryWithStats(builder, async (tx) => {
|
|
1247
|
-
await this.dropExistingIndexes(tx);
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* Given the current indexes in the optimizer, transform them in some
|
|
1252
|
-
* way to change which indexes will be assumed to exist when optimizing
|
|
1253
|
-
*
|
|
1254
|
-
* @example
|
|
1255
|
-
* ```
|
|
1256
|
-
* // resets indexes
|
|
1257
|
-
* optimizer.transformIndexes(() => [])
|
|
1258
|
-
*
|
|
1259
|
-
* // adds new index
|
|
1260
|
-
* optimizer.transformIndexes(indexes => [...indexes, newIndex])
|
|
1261
|
-
* ```
|
|
1262
|
-
*/
|
|
1263
|
-
transformIndexes(f) {
|
|
1264
|
-
const newIndexes = f(this.existingIndexes);
|
|
1265
|
-
this.existingIndexes = newIndexes;
|
|
1266
|
-
return this;
|
|
1267
|
-
}
|
|
1268
|
-
/**
|
|
1269
|
-
* Postgres has a limit of 63 characters for index names.
|
|
1270
|
-
* So we use this to make sure we don't derive it from a list of columns that can
|
|
1271
|
-
* overflow that limit.
|
|
1272
|
-
*/
|
|
1273
|
-
indexName() {
|
|
1274
|
-
const indexName = _IndexOptimizer.prefix + Math.random().toString(36).substring(2, 16);
|
|
1275
|
-
return PgIdentifier.fromString(indexName);
|
|
1276
|
-
}
|
|
1277
|
-
// TODO: this doesn't belong in the optimizer
|
|
1278
|
-
indexAlreadyExists(table, columns) {
|
|
1279
|
-
return this.existingIndexes.find(
|
|
1280
|
-
(index) => index.index_type === "btree" && index.table_name === table && index.index_columns.length === columns.length && index.index_columns.every((c, i) => {
|
|
1281
|
-
if (columns[i].column !== c.name) {
|
|
1282
|
-
return false;
|
|
1283
|
-
}
|
|
1284
|
-
if (columns[i].where) {
|
|
1285
|
-
return false;
|
|
1286
|
-
}
|
|
1287
|
-
if (columns[i].sort) {
|
|
1288
|
-
switch (columns[i].sort.dir) {
|
|
1289
|
-
// Sorting is ASC by default in postgres
|
|
1290
|
-
case "SORTBY_DEFAULT":
|
|
1291
|
-
case "SORTBY_ASC":
|
|
1292
|
-
if (c.order !== "ASC") {
|
|
1293
|
-
return false;
|
|
1294
|
-
}
|
|
1295
|
-
break;
|
|
1296
|
-
case "SORTBY_DESC":
|
|
1297
|
-
if (c.order !== "DESC") {
|
|
1298
|
-
return false;
|
|
1299
|
-
}
|
|
1300
|
-
break;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
return true;
|
|
1304
|
-
})
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
/**
|
|
1308
|
-
* Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
|
|
1309
|
-
**/
|
|
1310
|
-
indexesToCreate(rootCandidates) {
|
|
1311
|
-
const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
|
|
1312
|
-
const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
|
|
1313
|
-
const nextStage = [];
|
|
1314
|
-
const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
|
|
1315
|
-
for (const permutation of permutedIndexes.values()) {
|
|
1316
|
-
const { table: rawTable, schema: rawSchema, columns } = permutation;
|
|
1317
|
-
const permutations = permutationsWithDescendingLength(columns);
|
|
1318
|
-
for (const columns2 of permutations) {
|
|
1319
|
-
const schema = PgIdentifier.fromString(rawSchema);
|
|
1320
|
-
const table = PgIdentifier.fromString(rawTable);
|
|
1321
|
-
const existingIndex = this.indexAlreadyExists(
|
|
1322
|
-
table.toString(),
|
|
1323
|
-
columns2
|
|
1324
|
-
);
|
|
1325
|
-
if (existingIndex) {
|
|
1326
|
-
continue;
|
|
1327
|
-
}
|
|
1328
|
-
const indexName = this.indexName();
|
|
1329
|
-
const definition = this.toDefinition({ table, schema, columns: columns2 }).raw;
|
|
1330
|
-
nextStage.push({
|
|
1331
|
-
name: indexName,
|
|
1332
|
-
schema: schema.toString(),
|
|
1333
|
-
table: table.toString(),
|
|
1334
|
-
columns: columns2,
|
|
1335
|
-
definition
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
|
|
1340
|
-
for (const group of ginGroups.values()) {
|
|
1341
|
-
const { schema: rawSchema, table: rawTable, column, operators } = group;
|
|
1342
|
-
const schema = PgIdentifier.fromString(rawSchema);
|
|
1343
|
-
const table = PgIdentifier.fromString(rawTable);
|
|
1344
|
-
const needsKeyExistence = operators.some(
|
|
1345
|
-
(op) => op === "?" || op === "?|" || op === "?&"
|
|
1346
|
-
);
|
|
1347
|
-
const opclass = needsKeyExistence ? void 0 : "jsonb_path_ops";
|
|
1348
|
-
const existingGin = this.ginIndexAlreadyExists(table.toString(), column);
|
|
1349
|
-
if (existingGin) {
|
|
1350
|
-
continue;
|
|
1351
|
-
}
|
|
1352
|
-
const indexName = this.indexName();
|
|
1353
|
-
const candidate = {
|
|
1354
|
-
schema: rawSchema,
|
|
1355
|
-
table: rawTable,
|
|
1356
|
-
column
|
|
1357
|
-
};
|
|
1358
|
-
const definition = this.toGinDefinition({
|
|
1359
|
-
table,
|
|
1360
|
-
schema,
|
|
1361
|
-
column: PgIdentifier.fromString(column),
|
|
1362
|
-
opclass
|
|
1363
|
-
});
|
|
1364
|
-
nextStage.push({
|
|
1365
|
-
name: indexName,
|
|
1366
|
-
schema: schema.toString(),
|
|
1367
|
-
table: table.toString(),
|
|
1368
|
-
columns: [candidate],
|
|
1369
|
-
definition,
|
|
1370
|
-
indexMethod: "gin",
|
|
1371
|
-
opclass
|
|
1372
|
-
});
|
|
1373
|
-
}
|
|
1374
|
-
return nextStage;
|
|
1375
|
-
}
|
|
1376
|
-
toDefinition({
|
|
1377
|
-
schema,
|
|
1378
|
-
table,
|
|
1379
|
-
columns
|
|
1380
|
-
}) {
|
|
1381
|
-
const make = (col, order, where, keyword) => {
|
|
1382
|
-
let fullyQualifiedTable;
|
|
1383
|
-
if (schema.toString() === "public") {
|
|
1384
|
-
fullyQualifiedTable = table;
|
|
1385
|
-
} else {
|
|
1386
|
-
fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
|
|
1387
|
-
}
|
|
1388
|
-
const baseColumn = `${fullyQualifiedTable}(${columns.map((c) => {
|
|
1389
|
-
const column = PgIdentifier.fromString(c.column);
|
|
1390
|
-
const direction = c.sort && this.sortDirection(c.sort);
|
|
1391
|
-
const nulls = c.sort && this.nullsOrder(c.sort);
|
|
1392
|
-
let sort = col(column.toString());
|
|
1393
|
-
if (direction) {
|
|
1394
|
-
sort += ` ${order(direction)}`;
|
|
1395
|
-
}
|
|
1396
|
-
if (nulls) {
|
|
1397
|
-
sort += ` ${order(nulls)}`;
|
|
1398
|
-
}
|
|
1399
|
-
return sort;
|
|
1400
|
-
}).join(", ")})`;
|
|
1401
|
-
return baseColumn;
|
|
1402
|
-
};
|
|
1403
|
-
const id = (a) => a;
|
|
1404
|
-
const raw = make(id, id, id, id);
|
|
1405
|
-
const colored = make(green, yellow, magenta, blue2);
|
|
1406
|
-
return { raw, colored };
|
|
1407
|
-
}
|
|
1408
|
-
toGinDefinition({
|
|
1409
|
-
schema,
|
|
1410
|
-
table,
|
|
1411
|
-
column,
|
|
1412
|
-
opclass
|
|
1413
|
-
}) {
|
|
1414
|
-
let fullyQualifiedTable;
|
|
1415
|
-
if (schema.toString() === "public") {
|
|
1416
|
-
fullyQualifiedTable = table;
|
|
1417
|
-
} else {
|
|
1418
|
-
fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
|
|
1419
|
-
}
|
|
1420
|
-
const opclassSuffix = opclass ? ` ${opclass}` : "";
|
|
1421
|
-
return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
|
|
1422
|
-
}
|
|
1423
|
-
groupGinCandidatesByColumn(candidates) {
|
|
1424
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1425
|
-
for (const c of candidates) {
|
|
1426
|
-
if (!c.jsonbOperator) continue;
|
|
1427
|
-
const key = `${c.schema}.${c.table}.${c.column}`;
|
|
1428
|
-
const existing = groups.get(key);
|
|
1429
|
-
if (existing) {
|
|
1430
|
-
if (!existing.operators.includes(c.jsonbOperator)) {
|
|
1431
|
-
existing.operators.push(c.jsonbOperator);
|
|
1432
|
-
}
|
|
1433
|
-
} else {
|
|
1434
|
-
groups.set(key, {
|
|
1435
|
-
schema: c.schema,
|
|
1436
|
-
table: c.table,
|
|
1437
|
-
column: c.column,
|
|
1438
|
-
operators: [c.jsonbOperator]
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
return groups;
|
|
1443
|
-
}
|
|
1444
|
-
ginIndexAlreadyExists(table, column) {
|
|
1445
|
-
return this.existingIndexes.find(
|
|
1446
|
-
(index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column)
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
/**
|
|
1450
|
-
* Drop indexes that can be dropped. Ignore the ones that can't
|
|
1451
|
-
*/
|
|
1452
|
-
async dropExistingIndexes(tx) {
|
|
1453
|
-
for (const index of this.existingIndexes) {
|
|
1454
|
-
if (!isIndexProbablyDroppable(index)) {
|
|
1455
|
-
continue;
|
|
1456
|
-
}
|
|
1457
|
-
const indexName = PgIdentifier.fromParts(
|
|
1458
|
-
index.schema_name,
|
|
1459
|
-
index.index_name
|
|
1460
|
-
);
|
|
1461
|
-
await dropIndex(tx, indexName);
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
whereClause(c, col, keyword) {
|
|
1465
|
-
if (!c.where) {
|
|
1466
|
-
return "";
|
|
1467
|
-
}
|
|
1468
|
-
if (c.where.nulltest === "IS_NULL") {
|
|
1469
|
-
return `${col(`"${c.column}"`)} is ${keyword("null")}`;
|
|
1470
|
-
}
|
|
1471
|
-
if (c.where.nulltest === "IS_NOT_NULL") {
|
|
1472
|
-
return `${col(`"${c.column}"`)} is not ${keyword("null")}`;
|
|
1473
|
-
}
|
|
1474
|
-
return "";
|
|
1475
|
-
}
|
|
1476
|
-
nullsOrder(s) {
|
|
1477
|
-
if (!s.nulls) {
|
|
1478
|
-
return "";
|
|
1479
|
-
}
|
|
1480
|
-
switch (s.nulls) {
|
|
1481
|
-
case "SORTBY_NULLS_FIRST":
|
|
1482
|
-
return "nulls first";
|
|
1483
|
-
case "SORTBY_NULLS_LAST":
|
|
1484
|
-
return "nulls last";
|
|
1485
|
-
case "SORTBY_NULLS_DEFAULT":
|
|
1486
|
-
default:
|
|
1487
|
-
return "";
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
sortDirection(s) {
|
|
1491
|
-
if (!s.dir) {
|
|
1492
|
-
return "";
|
|
1493
|
-
}
|
|
1494
|
-
switch (s.dir) {
|
|
1495
|
-
case "SORTBY_DESC":
|
|
1496
|
-
return "desc";
|
|
1497
|
-
case "SORTBY_ASC":
|
|
1498
|
-
return "asc";
|
|
1499
|
-
case "SORTBY_DEFAULT":
|
|
1500
|
-
// god help us if we ever run into this
|
|
1501
|
-
case "SORTBY_USING":
|
|
1502
|
-
default:
|
|
1503
|
-
return "";
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
async testQueryWithStats(builder, f, options) {
|
|
1507
|
-
try {
|
|
1508
|
-
await this.db.transaction(async (tx) => {
|
|
1509
|
-
await f?.(tx);
|
|
1510
|
-
await this.statistics.restoreStats(tx);
|
|
1511
|
-
const flags = ["format json"];
|
|
1512
|
-
if (options && !options.genericPlan) {
|
|
1513
|
-
flags.push("analyze");
|
|
1514
|
-
if (this.config.trace) {
|
|
1515
|
-
flags.push("trace");
|
|
1516
|
-
}
|
|
1517
|
-
} else {
|
|
1518
|
-
flags.push("generic_plan");
|
|
1519
|
-
}
|
|
1520
|
-
const { commands, query } = builder.explain(flags).buildParts();
|
|
1521
|
-
await tx.exec(commands);
|
|
1522
|
-
const result = await tx.exec(
|
|
1523
|
-
query,
|
|
1524
|
-
options?.params
|
|
1525
|
-
);
|
|
1526
|
-
const explain = result[0]["QUERY PLAN"][0];
|
|
1527
|
-
throw new RollbackError(explain);
|
|
1528
|
-
});
|
|
1529
|
-
} catch (error) {
|
|
1530
|
-
if (error instanceof RollbackError) {
|
|
1531
|
-
return error.value;
|
|
1532
|
-
}
|
|
1533
|
-
throw error;
|
|
1534
|
-
}
|
|
1535
|
-
throw new Error("Unreachable");
|
|
1536
|
-
}
|
|
1537
|
-
groupPotentialIndexColumnsByTable(indexes) {
|
|
1538
|
-
const tableColumns = /* @__PURE__ */ new Map();
|
|
1539
|
-
for (const index of indexes) {
|
|
1540
|
-
const existing = tableColumns.get(`${index.schema}.${index.table}`);
|
|
1541
|
-
if (existing) {
|
|
1542
|
-
existing.columns.push(index);
|
|
1543
|
-
} else {
|
|
1544
|
-
tableColumns.set(`${index.schema}.${index.table}`, {
|
|
1545
|
-
table: index.table,
|
|
1546
|
-
schema: index.schema,
|
|
1547
|
-
columns: [index]
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
return tableColumns;
|
|
1552
|
-
}
|
|
1553
|
-
findUsedIndexes(explain) {
|
|
1554
|
-
const newIndexes = /* @__PURE__ */ new Set();
|
|
1555
|
-
const existingIndexes = /* @__PURE__ */ new Set();
|
|
1556
|
-
const prefix = _IndexOptimizer.prefix;
|
|
1557
|
-
walkExplain(explain, (stage) => {
|
|
1558
|
-
const indexName = stage["Index Name"];
|
|
1559
|
-
if (indexName) {
|
|
1560
|
-
if (indexName.startsWith(prefix)) {
|
|
1561
|
-
newIndexes.add(indexName);
|
|
1562
|
-
} else if (indexName.includes(prefix)) {
|
|
1563
|
-
const actualName = indexName.substring(indexName.indexOf(prefix));
|
|
1564
|
-
newIndexes.add(actualName);
|
|
1565
|
-
} else {
|
|
1566
|
-
existingIndexes.add(indexName);
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
});
|
|
1570
|
-
return {
|
|
1571
|
-
newIndexes,
|
|
1572
|
-
existingIndexes
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
replaceUsedIndexesWithDefinition(explain, triedIndexes) {
|
|
1576
|
-
walkExplain(explain, (stage) => {
|
|
1577
|
-
const indexName = stage["Index Name"];
|
|
1578
|
-
if (typeof indexName === "string") {
|
|
1579
|
-
const recommendation = triedIndexes.get(indexName);
|
|
1580
|
-
if (recommendation) {
|
|
1581
|
-
stage["Index Name"] = recommendation.definition;
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
};
|
|
1587
|
-
__publicField(_IndexOptimizer, "prefix", "__qd_");
|
|
1588
|
-
var IndexOptimizer = _IndexOptimizer;
|
|
1589
|
-
function walkExplain(explain, f) {
|
|
1590
|
-
function go(plan) {
|
|
1591
|
-
f(plan);
|
|
1592
|
-
if (plan.Plans) {
|
|
1593
|
-
for (const p of plan.Plans) {
|
|
1594
|
-
go(p);
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
go(explain);
|
|
1599
|
-
}
|
|
1600
|
-
var RollbackError = class {
|
|
1601
|
-
constructor(value) {
|
|
1602
|
-
this.value = value;
|
|
1603
|
-
}
|
|
1604
|
-
};
|
|
1605
|
-
var PROCEED = Symbol("PROCEED");
|
|
1606
|
-
var SKIP = Symbol("SKIP");
|
|
1607
|
-
|
|
1608
|
-
// src/optimizer/statistics.ts
|
|
1609
|
-
import { gray as gray2 } from "colorette";
|
|
1610
|
-
import dedent from "dedent";
|
|
1611
|
-
import { z as z2 } from "zod";
|
|
1612
|
-
var StatisticsSource = z2.union([
|
|
1613
|
-
z2.object({
|
|
1614
|
-
kind: z2.literal("path"),
|
|
1615
|
-
path: z2.string().min(1)
|
|
1616
|
-
}),
|
|
1617
|
-
z2.object({
|
|
1618
|
-
kind: z2.literal("inline")
|
|
1619
|
-
})
|
|
1620
|
-
]);
|
|
1621
|
-
var ExportedStatsStatistics = z2.object({
|
|
1622
|
-
stawidth: z2.number(),
|
|
1623
|
-
stainherit: z2.boolean().default(false),
|
|
1624
|
-
// 0 representing unknown
|
|
1625
|
-
stadistinct: z2.number(),
|
|
1626
|
-
// this has no "nullable" state
|
|
1627
|
-
stanullfrac: z2.number(),
|
|
1628
|
-
stakind1: z2.number().min(0),
|
|
1629
|
-
stakind2: z2.number().min(0),
|
|
1630
|
-
stakind3: z2.number().min(0),
|
|
1631
|
-
stakind4: z2.number().min(0),
|
|
1632
|
-
stakind5: z2.number().min(0),
|
|
1633
|
-
staop1: z2.string(),
|
|
1634
|
-
staop2: z2.string(),
|
|
1635
|
-
staop3: z2.string(),
|
|
1636
|
-
staop4: z2.string(),
|
|
1637
|
-
staop5: z2.string(),
|
|
1638
|
-
stacoll1: z2.string(),
|
|
1639
|
-
stacoll2: z2.string(),
|
|
1640
|
-
stacoll3: z2.string(),
|
|
1641
|
-
stacoll4: z2.string(),
|
|
1642
|
-
stacoll5: z2.string(),
|
|
1643
|
-
stanumbers1: z2.array(z2.number()).nullable(),
|
|
1644
|
-
stanumbers2: z2.array(z2.number()).nullable(),
|
|
1645
|
-
stanumbers3: z2.array(z2.number()).nullable(),
|
|
1646
|
-
stanumbers4: z2.array(z2.number()).nullable(),
|
|
1647
|
-
stanumbers5: z2.array(z2.number()).nullable(),
|
|
1648
|
-
// theoretically... this could only be strings and numbers
|
|
1649
|
-
// but we don't have a crystal ball
|
|
1650
|
-
stavalues1: z2.array(z2.any()).nullable(),
|
|
1651
|
-
stavalues2: z2.array(z2.any()).nullable(),
|
|
1652
|
-
stavalues3: z2.array(z2.any()).nullable(),
|
|
1653
|
-
stavalues4: z2.array(z2.any()).nullable(),
|
|
1654
|
-
stavalues5: z2.array(z2.any()).nullable()
|
|
1655
|
-
});
|
|
1656
|
-
var ExportedStatsColumns = z2.object({
|
|
1657
|
-
columnName: z2.string(),
|
|
1658
|
-
stats: ExportedStatsStatistics.nullable()
|
|
1659
|
-
});
|
|
1660
|
-
var ExportedStatsIndex = z2.object({
|
|
1661
|
-
indexName: z2.string(),
|
|
1662
|
-
relpages: z2.number(),
|
|
1663
|
-
reltuples: z2.number(),
|
|
1664
|
-
relallvisible: z2.number(),
|
|
1665
|
-
relallfrozen: z2.number().optional()
|
|
1666
|
-
});
|
|
1667
|
-
var ExportedStatsV1 = z2.object({
|
|
1668
|
-
tableName: z2.string(),
|
|
1669
|
-
schemaName: z2.string(),
|
|
1670
|
-
// can be negative
|
|
1671
|
-
relpages: z2.number(),
|
|
1672
|
-
// can be negative
|
|
1673
|
-
reltuples: z2.number(),
|
|
1674
|
-
relallvisible: z2.number(),
|
|
1675
|
-
// only postgres 18+
|
|
1676
|
-
relallfrozen: z2.number().optional(),
|
|
1677
|
-
columns: z2.array(ExportedStatsColumns).nullable(),
|
|
1678
|
-
indexes: z2.array(ExportedStatsIndex)
|
|
1679
|
-
});
|
|
1680
|
-
var ExportedStats = z2.union([ExportedStatsV1]);
|
|
1681
|
-
var StatisticsMode = z2.discriminatedUnion("kind", [
|
|
1682
|
-
z2.object({
|
|
1683
|
-
kind: z2.literal("fromAssumption"),
|
|
1684
|
-
reltuples: z2.number().min(0),
|
|
1685
|
-
relpages: z2.number().min(0)
|
|
1686
|
-
}),
|
|
1687
|
-
z2.object({
|
|
1688
|
-
kind: z2.literal("fromStatisticsExport"),
|
|
1689
|
-
stats: z2.array(ExportedStats),
|
|
1690
|
-
source: StatisticsSource
|
|
1691
|
-
})
|
|
1692
|
-
]);
|
|
1693
|
-
var DEFAULT_RELTUPLES = 1e4;
|
|
1694
|
-
var DEFAULT_RELPAGES = 1;
|
|
1695
|
-
var _Statistics = class _Statistics {
|
|
1696
|
-
constructor(db, postgresVersion, ownMetadata, statsMode) {
|
|
1697
|
-
this.db = db;
|
|
1698
|
-
this.postgresVersion = postgresVersion;
|
|
1699
|
-
this.ownMetadata = ownMetadata;
|
|
1700
|
-
__publicField(this, "mode");
|
|
1701
|
-
__publicField(this, "exportedMetadata");
|
|
1702
|
-
if (statsMode) {
|
|
1703
|
-
this.mode = statsMode;
|
|
1704
|
-
if (statsMode.kind === "fromStatisticsExport") {
|
|
1705
|
-
this.exportedMetadata = statsMode.stats;
|
|
1706
|
-
}
|
|
1707
|
-
} else {
|
|
1708
|
-
this.mode = _Statistics.defaultStatsMode;
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
static statsModeFromAssumption({
|
|
1712
|
-
reltuples,
|
|
1713
|
-
relpages
|
|
1714
|
-
}) {
|
|
1715
|
-
return {
|
|
1716
|
-
kind: "fromAssumption",
|
|
1717
|
-
reltuples,
|
|
1718
|
-
relpages
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
/**
|
|
1722
|
-
* Create a statistic mode from stats exported from another database
|
|
1723
|
-
**/
|
|
1724
|
-
static statsModeFromExport(stats) {
|
|
1725
|
-
return {
|
|
1726
|
-
kind: "fromStatisticsExport",
|
|
1727
|
-
source: { kind: "inline" },
|
|
1728
|
-
stats
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
|
-
static async fromPostgres(db, statsMode) {
|
|
1732
|
-
const version = await db.serverNum();
|
|
1733
|
-
const ownStats = await _Statistics.dumpStats(db, version, "full");
|
|
1734
|
-
return new _Statistics(db, version, ownStats, statsMode);
|
|
1735
|
-
}
|
|
1736
|
-
restoreStats(tx) {
|
|
1737
|
-
return this.restoreStats17(tx);
|
|
1738
|
-
}
|
|
1739
|
-
approximateTotalRows() {
|
|
1740
|
-
if (!this.exportedMetadata) {
|
|
1741
|
-
return 0;
|
|
1742
|
-
}
|
|
1743
|
-
let totalRows = 0;
|
|
1744
|
-
for (const table of this.exportedMetadata) {
|
|
1745
|
-
totalRows += table.reltuples;
|
|
1746
|
-
}
|
|
1747
|
-
return totalRows;
|
|
1748
|
-
}
|
|
1749
|
-
/**
|
|
1750
|
-
* We have to cast stavaluesN to the correct type
|
|
1751
|
-
* This derives that type for us so it can be used in `array_in`
|
|
1752
|
-
*/
|
|
1753
|
-
stavalueKind(values) {
|
|
1754
|
-
if (!values || values.length === 0) {
|
|
1755
|
-
return null;
|
|
1756
|
-
}
|
|
1757
|
-
const [elem] = values;
|
|
1758
|
-
if (typeof elem === "number") {
|
|
1759
|
-
return "real";
|
|
1760
|
-
} else if (typeof elem === "boolean") {
|
|
1761
|
-
return "boolean";
|
|
1762
|
-
}
|
|
1763
|
-
return "text";
|
|
1764
|
-
}
|
|
1765
|
-
/**
|
|
1766
|
-
* PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays
|
|
1767
|
-
* for columns with array types (e.g. text[], int4[]). These create
|
|
1768
|
-
* multidimensional arrays that can be "ragged" (sub-arrays with different
|
|
1769
|
-
* lengths). jsonb_to_recordset can't reconstruct ragged multidimensional
|
|
1770
|
-
* arrays from JSON, so we need to drop these values.
|
|
1771
|
-
*/
|
|
1772
|
-
static safeStavalues(values) {
|
|
1773
|
-
if (!values || values.length === 0) return values;
|
|
1774
|
-
if (values.some((v) => Array.isArray(v))) {
|
|
1775
|
-
console.warn("Discarding ragged multidimensional stavalues array");
|
|
1776
|
-
return null;
|
|
1777
|
-
}
|
|
1778
|
-
return values;
|
|
1779
|
-
}
|
|
1780
|
-
async restoreStats17(tx) {
|
|
1781
|
-
const warnings = {
|
|
1782
|
-
tablesNotInExports: [],
|
|
1783
|
-
tablesNotInTest: [],
|
|
1784
|
-
tableNotAnalyzed: [],
|
|
1785
|
-
statsMissing: []
|
|
1786
|
-
};
|
|
1787
|
-
const processedTables = /* @__PURE__ */ new Set();
|
|
1788
|
-
let columnStatsUpdatePromise;
|
|
1789
|
-
const columnStatsValues = [];
|
|
1790
|
-
if (this.exportedMetadata) {
|
|
1791
|
-
for (const table of this.ownMetadata) {
|
|
1792
|
-
const targetTable = this.exportedMetadata.find(
|
|
1793
|
-
(m) => m.tableName === table.tableName && m.schemaName === table.schemaName
|
|
1794
|
-
);
|
|
1795
|
-
if (!targetTable?.columns) {
|
|
1796
|
-
continue;
|
|
1797
|
-
}
|
|
1798
|
-
for (const column of targetTable.columns) {
|
|
1799
|
-
const { stats } = column;
|
|
1800
|
-
if (!stats) {
|
|
1801
|
-
continue;
|
|
1802
|
-
}
|
|
1803
|
-
columnStatsValues.push({
|
|
1804
|
-
schema_name: table.schemaName,
|
|
1805
|
-
table_name: table.tableName,
|
|
1806
|
-
column_name: column.columnName,
|
|
1807
|
-
stainherit: stats.stainherit ?? false,
|
|
1808
|
-
stanullfrac: stats.stanullfrac,
|
|
1809
|
-
stawidth: stats.stawidth,
|
|
1810
|
-
stadistinct: stats.stadistinct,
|
|
1811
|
-
stakind1: stats.stakind1,
|
|
1812
|
-
stakind2: stats.stakind2,
|
|
1813
|
-
stakind3: stats.stakind3,
|
|
1814
|
-
stakind4: stats.stakind4,
|
|
1815
|
-
stakind5: stats.stakind5,
|
|
1816
|
-
staop1: stats.staop1,
|
|
1817
|
-
staop2: stats.staop2,
|
|
1818
|
-
staop3: stats.staop3,
|
|
1819
|
-
staop4: stats.staop4,
|
|
1820
|
-
staop5: stats.staop5,
|
|
1821
|
-
stacoll1: stats.stacoll1,
|
|
1822
|
-
stacoll2: stats.stacoll2,
|
|
1823
|
-
stacoll3: stats.stacoll3,
|
|
1824
|
-
stacoll4: stats.stacoll4,
|
|
1825
|
-
stacoll5: stats.stacoll5,
|
|
1826
|
-
stanumbers1: stats.stanumbers1,
|
|
1827
|
-
stanumbers2: stats.stanumbers2,
|
|
1828
|
-
stanumbers3: stats.stanumbers3,
|
|
1829
|
-
stanumbers4: stats.stanumbers4,
|
|
1830
|
-
stanumbers5: stats.stanumbers5,
|
|
1831
|
-
stavalues1: _Statistics.safeStavalues(stats.stavalues1),
|
|
1832
|
-
stavalues2: _Statistics.safeStavalues(stats.stavalues2),
|
|
1833
|
-
stavalues3: _Statistics.safeStavalues(stats.stavalues3),
|
|
1834
|
-
stavalues4: _Statistics.safeStavalues(stats.stavalues4),
|
|
1835
|
-
stavalues5: _Statistics.safeStavalues(stats.stavalues5),
|
|
1836
|
-
_value_type1: this.stavalueKind(
|
|
1837
|
-
_Statistics.safeStavalues(stats.stavalues1)
|
|
1838
|
-
),
|
|
1839
|
-
_value_type2: this.stavalueKind(
|
|
1840
|
-
_Statistics.safeStavalues(stats.stavalues2)
|
|
1841
|
-
),
|
|
1842
|
-
_value_type3: this.stavalueKind(
|
|
1843
|
-
_Statistics.safeStavalues(stats.stavalues3)
|
|
1844
|
-
),
|
|
1845
|
-
_value_type4: this.stavalueKind(
|
|
1846
|
-
_Statistics.safeStavalues(stats.stavalues4)
|
|
1847
|
-
),
|
|
1848
|
-
_value_type5: this.stavalueKind(
|
|
1849
|
-
_Statistics.safeStavalues(stats.stavalues5)
|
|
1850
|
-
)
|
|
1851
|
-
});
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
const sql = dedent`
|
|
1855
|
-
WITH input AS (
|
|
1856
|
-
SELECT
|
|
1857
|
-
c.oid AS starelid,
|
|
1858
|
-
a.attnum AS staattnum,
|
|
1859
|
-
v.stainherit,
|
|
1860
|
-
v.stanullfrac,
|
|
1861
|
-
v.stawidth,
|
|
1862
|
-
v.stadistinct,
|
|
1863
|
-
v.stakind1,
|
|
1864
|
-
v.stakind2,
|
|
1865
|
-
v.stakind3,
|
|
1866
|
-
v.stakind4,
|
|
1867
|
-
v.stakind5,
|
|
1868
|
-
v.staop1,
|
|
1869
|
-
v.staop2,
|
|
1870
|
-
v.staop3,
|
|
1871
|
-
v.staop4,
|
|
1872
|
-
v.staop5,
|
|
1873
|
-
v.stacoll1,
|
|
1874
|
-
v.stacoll2,
|
|
1875
|
-
v.stacoll3,
|
|
1876
|
-
v.stacoll4,
|
|
1877
|
-
v.stacoll5,
|
|
1878
|
-
v.stanumbers1,
|
|
1879
|
-
v.stanumbers2,
|
|
1880
|
-
v.stanumbers3,
|
|
1881
|
-
v.stanumbers4,
|
|
1882
|
-
v.stanumbers5,
|
|
1883
|
-
v.stavalues1,
|
|
1884
|
-
v.stavalues2,
|
|
1885
|
-
v.stavalues3,
|
|
1886
|
-
v.stavalues4,
|
|
1887
|
-
v.stavalues5,
|
|
1888
|
-
_value_type1,
|
|
1889
|
-
_value_type2,
|
|
1890
|
-
_value_type3,
|
|
1891
|
-
_value_type4,
|
|
1892
|
-
_value_type5
|
|
1893
|
-
FROM jsonb_to_recordset($1::jsonb) AS v(
|
|
1894
|
-
schema_name text,
|
|
1895
|
-
table_name text,
|
|
1896
|
-
column_name text,
|
|
1897
|
-
stainherit boolean,
|
|
1898
|
-
stanullfrac real,
|
|
1899
|
-
stawidth integer,
|
|
1900
|
-
stadistinct real,
|
|
1901
|
-
stakind1 real,
|
|
1902
|
-
stakind2 real,
|
|
1903
|
-
stakind3 real,
|
|
1904
|
-
stakind4 real,
|
|
1905
|
-
stakind5 real,
|
|
1906
|
-
staop1 oid,
|
|
1907
|
-
staop2 oid,
|
|
1908
|
-
staop3 oid,
|
|
1909
|
-
staop4 oid,
|
|
1910
|
-
staop5 oid,
|
|
1911
|
-
stacoll1 oid,
|
|
1912
|
-
stacoll2 oid,
|
|
1913
|
-
stacoll3 oid,
|
|
1914
|
-
stacoll4 oid,
|
|
1915
|
-
stacoll5 oid,
|
|
1916
|
-
stanumbers1 real[],
|
|
1917
|
-
stanumbers2 real[],
|
|
1918
|
-
stanumbers3 real[],
|
|
1919
|
-
stanumbers4 real[],
|
|
1920
|
-
stanumbers5 real[],
|
|
1921
|
-
stavalues1 text[],
|
|
1922
|
-
stavalues2 text[],
|
|
1923
|
-
stavalues3 text[],
|
|
1924
|
-
stavalues4 text[],
|
|
1925
|
-
stavalues5 text[],
|
|
1926
|
-
_value_type1 text,
|
|
1927
|
-
_value_type2 text,
|
|
1928
|
-
_value_type3 text,
|
|
1929
|
-
_value_type4 text,
|
|
1930
|
-
_value_type5 text
|
|
1931
|
-
)
|
|
1932
|
-
JOIN pg_class c ON c.relname = v.table_name
|
|
1933
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = v.schema_name
|
|
1934
|
-
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = v.column_name
|
|
1935
|
-
),
|
|
1936
|
-
updated AS (
|
|
1937
|
-
UPDATE pg_statistic s
|
|
1938
|
-
SET
|
|
1939
|
-
stanullfrac = i.stanullfrac,
|
|
1940
|
-
stawidth = i.stawidth,
|
|
1941
|
-
stadistinct = i.stadistinct,
|
|
1942
|
-
stakind1 = i.stakind1,
|
|
1943
|
-
stakind2 = i.stakind2,
|
|
1944
|
-
stakind3 = i.stakind3,
|
|
1945
|
-
stakind4 = i.stakind4,
|
|
1946
|
-
stakind5 = i.stakind5,
|
|
1947
|
-
staop1 = i.staop1,
|
|
1948
|
-
staop2 = i.staop2,
|
|
1949
|
-
staop3 = i.staop3,
|
|
1950
|
-
staop4 = i.staop4,
|
|
1951
|
-
staop5 = i.staop5,
|
|
1952
|
-
stacoll1 = i.stacoll1,
|
|
1953
|
-
stacoll2 = i.stacoll2,
|
|
1954
|
-
stacoll3 = i.stacoll3,
|
|
1955
|
-
stacoll4 = i.stacoll4,
|
|
1956
|
-
stacoll5 = i.stacoll5,
|
|
1957
|
-
stanumbers1 = i.stanumbers1,
|
|
1958
|
-
stanumbers2 = i.stanumbers2,
|
|
1959
|
-
stanumbers3 = i.stanumbers3,
|
|
1960
|
-
stanumbers4 = i.stanumbers4,
|
|
1961
|
-
stanumbers5 = i.stanumbers5,
|
|
1962
|
-
stavalues1 = case
|
|
1963
|
-
when i.stavalues1 is null then null
|
|
1964
|
-
else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)
|
|
1965
|
-
end,
|
|
1966
|
-
stavalues2 = case
|
|
1967
|
-
when i.stavalues2 is null then null
|
|
1968
|
-
else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)
|
|
1969
|
-
end,
|
|
1970
|
-
stavalues3 = case
|
|
1971
|
-
when i.stavalues3 is null then null
|
|
1972
|
-
else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)
|
|
1973
|
-
end,
|
|
1974
|
-
stavalues4 = case
|
|
1975
|
-
when i.stavalues4 is null then null
|
|
1976
|
-
else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)
|
|
1977
|
-
end,
|
|
1978
|
-
stavalues5 = case
|
|
1979
|
-
when i.stavalues5 is null then null
|
|
1980
|
-
else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)
|
|
1981
|
-
end
|
|
1982
|
-
-- stavalues1 = i.stavalues1,
|
|
1983
|
-
-- stavalues2 = i.stavalues2,
|
|
1984
|
-
-- stavalues3 = i.stavalues3,
|
|
1985
|
-
-- stavalues4 = i.stavalues4,
|
|
1986
|
-
-- stavalues5 = i.stavalues5
|
|
1987
|
-
FROM input i
|
|
1988
|
-
WHERE s.starelid = i.starelid AND s.staattnum = i.staattnum AND s.stainherit = i.stainherit
|
|
1989
|
-
RETURNING s.starelid, s.staattnum, s.stainherit, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5
|
|
1990
|
-
),
|
|
1991
|
-
inserted as (
|
|
1992
|
-
INSERT INTO pg_statistic (
|
|
1993
|
-
starelid, staattnum, stainherit,
|
|
1994
|
-
stanullfrac, stawidth, stadistinct,
|
|
1995
|
-
stakind1, stakind2, stakind3, stakind4, stakind5,
|
|
1996
|
-
staop1, staop2, staop3, staop4, staop5,
|
|
1997
|
-
stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
|
|
1998
|
-
stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
|
|
1999
|
-
stavalues1, stavalues2, stavalues3, stavalues4, stavalues5
|
|
2000
|
-
)
|
|
2001
|
-
SELECT
|
|
2002
|
-
i.starelid, i.staattnum, i.stainherit,
|
|
2003
|
-
i.stanullfrac, i.stawidth, i.stadistinct,
|
|
2004
|
-
i.stakind1, i.stakind2, i.stakind3, i.stakind4, i.stakind5,
|
|
2005
|
-
i.staop1, i.staop2, i.staop3, i.staop4, i.staop5,
|
|
2006
|
-
i.stacoll1, i.stacoll2, i.stacoll3, i.stacoll4, i.stacoll5,
|
|
2007
|
-
i.stanumbers1, i.stanumbers2, i.stanumbers3, i.stanumbers4, i.stanumbers5,
|
|
2008
|
-
-- i.stavalues1, i.stavalues2, i.stavalues3, i.stavalues4, i.stavalues5,
|
|
2009
|
-
case
|
|
2010
|
-
when i.stavalues1 is null then null
|
|
2011
|
-
else array_in(i.stavalues1::text::cstring, i._value_type1::regtype::oid, -1)
|
|
2012
|
-
end,
|
|
2013
|
-
case
|
|
2014
|
-
when i.stavalues2 is null then null
|
|
2015
|
-
else array_in(i.stavalues2::text::cstring, i._value_type2::regtype::oid, -1)
|
|
2016
|
-
end,
|
|
2017
|
-
case
|
|
2018
|
-
when i.stavalues3 is null then null
|
|
2019
|
-
else array_in(i.stavalues3::text::cstring, i._value_type3::regtype::oid, -1)
|
|
2020
|
-
end,
|
|
2021
|
-
case
|
|
2022
|
-
when i.stavalues4 is null then null
|
|
2023
|
-
else array_in(i.stavalues4::text::cstring, i._value_type4::regtype::oid, -1)
|
|
2024
|
-
end,
|
|
2025
|
-
case
|
|
2026
|
-
when i.stavalues5 is null then null
|
|
2027
|
-
else array_in(i.stavalues5::text::cstring, i._value_type5::regtype::oid, -1)
|
|
2028
|
-
end
|
|
2029
|
-
-- i._value_type1, i._value_type2, i._value_type3, i._value_type4, i._value_type5
|
|
2030
|
-
FROM input i
|
|
2031
|
-
LEFT JOIN updated u
|
|
2032
|
-
ON i.starelid = u.starelid AND i.staattnum = u.staattnum AND i.stainherit = u.stainherit
|
|
2033
|
-
WHERE u.starelid IS NULL
|
|
2034
|
-
returning starelid, staattnum, stainherit, stakind1, stakind2, stakind3, stakind4, stakind5
|
|
2035
|
-
)
|
|
2036
|
-
select * from updated union all (select * from inserted); -- @qd_introspection`;
|
|
2037
|
-
columnStatsUpdatePromise = tx.exec(sql, [columnStatsValues]).catch((err) => {
|
|
2038
|
-
console.error("Something wrong wrong updating column stats");
|
|
2039
|
-
console.error(err);
|
|
2040
|
-
throw err;
|
|
2041
|
-
});
|
|
2042
|
-
}
|
|
2043
|
-
const reltuplesValues = [];
|
|
2044
|
-
for (const table of this.ownMetadata) {
|
|
2045
|
-
if (!table.columns) {
|
|
2046
|
-
continue;
|
|
2047
|
-
}
|
|
2048
|
-
processedTables.add(`${table.schemaName}.${table.tableName}`);
|
|
2049
|
-
let targetTable;
|
|
2050
|
-
if (this.exportedMetadata) {
|
|
2051
|
-
targetTable = this.exportedMetadata.find(
|
|
2052
|
-
(m) => m.tableName === table.tableName && m.schemaName === table.schemaName
|
|
2053
|
-
);
|
|
2054
|
-
}
|
|
2055
|
-
let reltuples;
|
|
2056
|
-
let relpages;
|
|
2057
|
-
let relallvisible = 0;
|
|
2058
|
-
let relallfrozen;
|
|
2059
|
-
if (targetTable) {
|
|
2060
|
-
reltuples = targetTable.reltuples;
|
|
2061
|
-
relpages = targetTable.relpages;
|
|
2062
|
-
relallvisible = targetTable.relallvisible;
|
|
2063
|
-
relallfrozen = targetTable.relallfrozen;
|
|
2064
|
-
} else if (this.mode.kind === "fromAssumption") {
|
|
2065
|
-
reltuples = this.mode.reltuples;
|
|
2066
|
-
relpages = this.mode.relpages;
|
|
2067
|
-
} else {
|
|
2068
|
-
warnings.tablesNotInExports.push(
|
|
2069
|
-
`${table.schemaName}.${table.tableName}`
|
|
2070
|
-
);
|
|
2071
|
-
reltuples = DEFAULT_RELTUPLES;
|
|
2072
|
-
relpages = DEFAULT_RELPAGES;
|
|
2073
|
-
}
|
|
2074
|
-
reltuplesValues.push({
|
|
2075
|
-
relname: table.tableName,
|
|
2076
|
-
schema_name: table.schemaName,
|
|
2077
|
-
reltuples,
|
|
2078
|
-
relpages,
|
|
2079
|
-
relallfrozen,
|
|
2080
|
-
relallvisible
|
|
2081
|
-
});
|
|
2082
|
-
if (targetTable && targetTable.indexes) {
|
|
2083
|
-
for (const index of targetTable.indexes) {
|
|
2084
|
-
reltuplesValues.push({
|
|
2085
|
-
relname: index.indexName,
|
|
2086
|
-
schema_name: targetTable.schemaName,
|
|
2087
|
-
reltuples: index.reltuples,
|
|
2088
|
-
relpages: index.relpages,
|
|
2089
|
-
relallfrozen: index.relallfrozen,
|
|
2090
|
-
relallvisible: index.relallvisible
|
|
2091
|
-
});
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
const reltuplesQuery = dedent`
|
|
2096
|
-
update pg_class p
|
|
2097
|
-
set reltuples = v.reltuples,
|
|
2098
|
-
relpages = v.relpages,
|
|
2099
|
-
-- relallfrozen = case when v.relallfrozen is null then p.relallfrozen else v.relallfrozen end,
|
|
2100
|
-
relallvisible = case when v.relallvisible is null then p.relallvisible else v.relallvisible end
|
|
2101
|
-
from jsonb_to_recordset($1::jsonb)
|
|
2102
|
-
as v(reltuples real, relpages integer, relallfrozen integer, relallvisible integer, relname text, schema_name text)
|
|
2103
|
-
where p.relname = v.relname
|
|
2104
|
-
and p.relnamespace = (select oid from pg_namespace where nspname = v.schema_name)
|
|
2105
|
-
returning p.relname, p.relnamespace, p.reltuples, p.relpages;
|
|
2106
|
-
`;
|
|
2107
|
-
const reltuplesPromise = tx.exec(reltuplesQuery, [reltuplesValues]).catch((err) => {
|
|
2108
|
-
console.error("Something went wrong updating reltuples/relpages");
|
|
2109
|
-
console.error(err);
|
|
2110
|
-
return err;
|
|
2111
|
-
});
|
|
2112
|
-
if (this.exportedMetadata) {
|
|
2113
|
-
for (const table of this.exportedMetadata) {
|
|
2114
|
-
const tableExists = processedTables.has(
|
|
2115
|
-
`${table.schemaName}.${table.tableName}`
|
|
2116
|
-
);
|
|
2117
|
-
if (tableExists && table.reltuples === -1) {
|
|
2118
|
-
console.warn(
|
|
2119
|
-
`Table ${table.tableName} has reltuples -1. Your production database is probably not analyzed properly`
|
|
2120
|
-
);
|
|
2121
|
-
warnings.tableNotAnalyzed.push(
|
|
2122
|
-
`${table.schemaName}.${table.tableName}`
|
|
2123
|
-
);
|
|
2124
|
-
}
|
|
2125
|
-
if (tableExists) {
|
|
2126
|
-
continue;
|
|
2127
|
-
}
|
|
2128
|
-
warnings.tablesNotInTest.push(`${table.schemaName}.${table.tableName}`);
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
const [statsUpdates, reltuplesUpdates] = await Promise.all([
|
|
2132
|
-
columnStatsUpdatePromise,
|
|
2133
|
-
reltuplesPromise
|
|
2134
|
-
]);
|
|
2135
|
-
const updatedColumnsProperly = statsUpdates ? statsUpdates.length === columnStatsValues.length : true;
|
|
2136
|
-
if (!updatedColumnsProperly) {
|
|
2137
|
-
console.error(`Did not update expected column stats`);
|
|
2138
|
-
}
|
|
2139
|
-
if (reltuplesUpdates.length !== reltuplesValues.length) {
|
|
2140
|
-
console.error(`Did not update expected reltuples/relpages`);
|
|
2141
|
-
}
|
|
2142
|
-
return warnings;
|
|
2143
|
-
}
|
|
2144
|
-
static async dumpStats(db, postgresVersion, kind) {
|
|
2145
|
-
const fullDump = kind === "full";
|
|
2146
|
-
console.log(`dumping stats for postgres ${gray2(postgresVersion)}`);
|
|
2147
|
-
const stats = await db.exec(
|
|
2148
|
-
`
|
|
2149
|
-
WITH table_columns AS (
|
|
2150
|
-
SELECT
|
|
2151
|
-
c.table_name,
|
|
2152
|
-
c.table_schema,
|
|
2153
|
-
cl.reltuples,
|
|
2154
|
-
cl.relpages,
|
|
2155
|
-
cl.relallvisible,
|
|
2156
|
-
-- cl.relallfrozen,
|
|
2157
|
-
n.nspname AS schema_name,
|
|
2158
|
-
json_agg(
|
|
2159
|
-
json_build_object(
|
|
2160
|
-
'columnName', c.column_name,
|
|
2161
|
-
'stats', (
|
|
2162
|
-
SELECT json_build_object(
|
|
2163
|
-
'starelid', s.starelid,
|
|
2164
|
-
'staattnum', s.staattnum,
|
|
2165
|
-
'stanullfrac', s.stanullfrac,
|
|
2166
|
-
'stawidth', s.stawidth,
|
|
2167
|
-
'stadistinct', s.stadistinct,
|
|
2168
|
-
'stakind1', s.stakind1, 'staop1', s.staop1, 'stacoll1', s.stacoll1, 'stanumbers1', s.stanumbers1,
|
|
2169
|
-
'stakind2', s.stakind2, 'staop2', s.staop2, 'stacoll2', s.stacoll2, 'stanumbers2', s.stanumbers2,
|
|
2170
|
-
'stakind3', s.stakind3, 'staop3', s.staop3, 'stacoll3', s.stacoll3, 'stanumbers3', s.stanumbers3,
|
|
2171
|
-
'stakind4', s.stakind4, 'staop4', s.staop4, 'stacoll4', s.stacoll4, 'stanumbers4', s.stanumbers4,
|
|
2172
|
-
'stakind5', s.stakind5, 'staop5', s.staop5, 'stacoll5', s.stacoll5, 'stanumbers5', s.stanumbers5,
|
|
2173
|
-
'stavalues1', CASE WHEN $1 THEN s.stavalues1 ELSE NULL END,
|
|
2174
|
-
'stavalues2', CASE WHEN $1 THEN s.stavalues2 ELSE NULL END,
|
|
2175
|
-
'stavalues3', CASE WHEN $1 THEN s.stavalues3 ELSE NULL END,
|
|
2176
|
-
'stavalues4', CASE WHEN $1 THEN s.stavalues4 ELSE NULL END,
|
|
2177
|
-
'stavalues5', CASE WHEN $1 THEN s.stavalues5 ELSE NULL END
|
|
2178
|
-
)
|
|
2179
|
-
FROM pg_statistic s
|
|
2180
|
-
WHERE s.starelid = a.attrelid AND s.staattnum = a.attnum
|
|
2181
|
-
)
|
|
2182
|
-
)
|
|
2183
|
-
ORDER BY c.ordinal_position
|
|
2184
|
-
) AS columns
|
|
2185
|
-
FROM information_schema.columns c
|
|
2186
|
-
JOIN pg_attribute a
|
|
2187
|
-
ON a.attrelid = (quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass
|
|
2188
|
-
AND a.attname = c.column_name
|
|
2189
|
-
JOIN pg_class cl
|
|
2190
|
-
ON cl.oid = a.attrelid
|
|
2191
|
-
JOIN pg_namespace n
|
|
2192
|
-
ON n.oid = cl.relnamespace
|
|
2193
|
-
WHERE c.table_name NOT LIKE 'pg_%'
|
|
2194
|
-
AND n.nspname <> 'information_schema'
|
|
2195
|
-
AND c.table_name NOT IN ('pg_stat_statements', 'pg_stat_statements_info')
|
|
2196
|
-
GROUP BY c.table_name, c.table_schema, cl.reltuples, cl.relpages, cl.relallvisible, n.nspname
|
|
2197
|
-
),
|
|
2198
|
-
table_indexes AS (
|
|
2199
|
-
SELECT
|
|
2200
|
-
t.relname AS table_name,
|
|
2201
|
-
json_agg(
|
|
2202
|
-
json_build_object(
|
|
2203
|
-
'indexName', i.relname,
|
|
2204
|
-
'reltuples', i.reltuples,
|
|
2205
|
-
'relpages', i.relpages,
|
|
2206
|
-
'relallvisible', i.relallvisible
|
|
2207
|
-
-- 'relallfrozen', i.relallfrozen
|
|
2208
|
-
)
|
|
2209
|
-
) AS indexes
|
|
2210
|
-
FROM pg_class t
|
|
2211
|
-
JOIN pg_index ix ON ix.indrelid = t.oid
|
|
2212
|
-
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
2213
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
2214
|
-
WHERE t.relname NOT LIKE 'pg_%'
|
|
2215
|
-
AND n.nspname <> 'information_schema'
|
|
2216
|
-
GROUP BY t.relname
|
|
2217
|
-
)
|
|
2218
|
-
SELECT json_agg(
|
|
2219
|
-
json_build_object(
|
|
2220
|
-
'tableName', tc.table_name,
|
|
2221
|
-
'schemaName', tc.table_schema,
|
|
2222
|
-
'reltuples', tc.reltuples,
|
|
2223
|
-
'relpages', tc.relpages,
|
|
2224
|
-
'relallvisible', tc.relallvisible,
|
|
2225
|
-
-- 'relallfrozen', tc.relallfrozen,
|
|
2226
|
-
'columns', tc.columns,
|
|
2227
|
-
'indexes', COALESCE(ti.indexes, '[]'::json)
|
|
2228
|
-
)
|
|
2229
|
-
)
|
|
2230
|
-
FROM table_columns tc
|
|
2231
|
-
LEFT JOIN table_indexes ti
|
|
2232
|
-
ON ti.table_name = tc.table_name;
|
|
2233
|
-
`,
|
|
2234
|
-
[fullDump]
|
|
2235
|
-
);
|
|
2236
|
-
return stats[0].json_agg;
|
|
2237
|
-
}
|
|
2238
|
-
/**
|
|
2239
|
-
* Returns all indexes in the database.
|
|
2240
|
-
* ONLY handles regular btree indexes
|
|
2241
|
-
*/
|
|
2242
|
-
async getExistingIndexes() {
|
|
2243
|
-
const indexes = await this.db.exec(`
|
|
2244
|
-
WITH partitioned_tables AS (
|
|
2245
|
-
SELECT
|
|
2246
|
-
inhparent::regclass AS parent_table,
|
|
2247
|
-
inhrelid::regclass AS partition_table
|
|
2248
|
-
FROM
|
|
2249
|
-
pg_inherits
|
|
2250
|
-
)
|
|
2251
|
-
SELECT
|
|
2252
|
-
n.nspname AS schema_name,
|
|
2253
|
-
COALESCE(pt.parent_table::text, t.relname) AS table_name,
|
|
2254
|
-
i.relname AS index_name,
|
|
2255
|
-
ix.indisprimary as is_primary,
|
|
2256
|
-
ix.indisunique as is_unique,
|
|
2257
|
-
am.amname AS index_type,
|
|
2258
|
-
array_agg(
|
|
2259
|
-
CASE
|
|
2260
|
-
-- Handle regular columns
|
|
2261
|
-
WHEN a.attname IS NOT NULL THEN
|
|
2262
|
-
json_build_object('name', a.attname, 'order',
|
|
2263
|
-
CASE
|
|
2264
|
-
WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
|
|
2265
|
-
ELSE 'ASC'
|
|
2266
|
-
END,
|
|
2267
|
-
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2268
|
-
-- Handle expressions
|
|
2269
|
-
ELSE
|
|
2270
|
-
json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
|
|
2271
|
-
CASE
|
|
2272
|
-
WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
|
|
2273
|
-
ELSE 'ASC'
|
|
2274
|
-
END,
|
|
2275
|
-
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2276
|
-
END
|
|
2277
|
-
ORDER BY array_position(ix.indkey, k.attnum)
|
|
2278
|
-
) AS index_columns
|
|
2279
|
-
FROM
|
|
2280
|
-
pg_class t
|
|
2281
|
-
LEFT JOIN partitioned_tables pt ON t.oid = pt.partition_table
|
|
2282
|
-
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
2283
|
-
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
2284
|
-
JOIN pg_am am ON i.relam = am.oid
|
|
2285
|
-
LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
|
|
2286
|
-
LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
|
|
2287
|
-
LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]
|
|
2288
|
-
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
2289
|
-
WHERE
|
|
2290
|
-
n.nspname not like 'pg_%' and
|
|
2291
|
-
n.nspname <> 'information_schema'
|
|
2292
|
-
GROUP BY
|
|
2293
|
-
n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary, ix.indisunique
|
|
2294
|
-
ORDER BY
|
|
2295
|
-
COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection
|
|
2296
|
-
`);
|
|
2297
|
-
return indexes;
|
|
2298
|
-
}
|
|
2299
|
-
};
|
|
2300
|
-
// preventing accidental internal mutations
|
|
2301
|
-
__publicField(_Statistics, "defaultStatsMode", Object.freeze({
|
|
2302
|
-
kind: "fromAssumption",
|
|
2303
|
-
reltuples: DEFAULT_RELTUPLES,
|
|
2304
|
-
relpages: DEFAULT_RELPAGES
|
|
2305
|
-
}));
|
|
2306
|
-
var Statistics = _Statistics;
|
|
2307
|
-
|
|
2308
|
-
// src/optimizer/pss-rewriter.ts
|
|
2309
|
-
var PssRewriter = class {
|
|
2310
|
-
constructor() {
|
|
2311
|
-
__publicField(this, "problematicKeywords", ["interval", "timestamp", "geometry"]);
|
|
2312
|
-
}
|
|
2313
|
-
rewrite(query) {
|
|
2314
|
-
return this.rewriteKeywordWithParameter(query);
|
|
2315
|
-
}
|
|
2316
|
-
rewriteKeywordWithParameter(query) {
|
|
2317
|
-
return query.replace(/\b(\w+) (\$\d+)\b/gi, (match) => {
|
|
2318
|
-
const [keyword, parameter] = match.split(" ");
|
|
2319
|
-
const isProblematicKeyword = this.problematicKeywords.includes(
|
|
2320
|
-
keyword.toLowerCase()
|
|
2321
|
-
);
|
|
2322
|
-
if (!isProblematicKeyword) {
|
|
2323
|
-
return match;
|
|
2324
|
-
}
|
|
2325
|
-
return `(${parameter}::${keyword.toLowerCase()})`;
|
|
2326
|
-
});
|
|
2327
|
-
}
|
|
2328
|
-
};
|
|
2329
|
-
export {
|
|
2330
|
-
Analyzer,
|
|
2331
|
-
ExportedStats,
|
|
2332
|
-
ExportedStatsColumns,
|
|
2333
|
-
ExportedStatsIndex,
|
|
2334
|
-
ExportedStatsStatistics,
|
|
2335
|
-
ExportedStatsV1,
|
|
2336
|
-
IndexOptimizer,
|
|
2337
|
-
PROCEED,
|
|
2338
|
-
PgIdentifier,
|
|
2339
|
-
PostgresQueryBuilder,
|
|
2340
|
-
PostgresVersion,
|
|
2341
|
-
PssRewriter,
|
|
2342
|
-
SKIP,
|
|
2343
|
-
Statistics,
|
|
2344
|
-
StatisticsMode,
|
|
2345
|
-
StatisticsSource,
|
|
2346
|
-
dropIndex,
|
|
2347
|
-
ignoredIdentifier,
|
|
2348
|
-
isIndexProbablyDroppable,
|
|
2349
|
-
isIndexSupported,
|
|
2350
|
-
parseNudges
|
|
2351
|
-
};
|
|
2352
|
-
//# sourceMappingURL=index.js.map
|