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