@query-doctor/core 0.6.0-rc.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +178 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +6 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +171 -107
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value:
|
|
1
|
+
"use client";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
//#region \0rolldown/runtime.js
|
|
4
4
|
var __create = Object.create;
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
@@ -8,16 +8,12 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
8
8
|
var __getProtoOf = Object.getPrototypeOf;
|
|
9
9
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
10
|
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
21
17
|
}
|
|
22
18
|
return to;
|
|
23
19
|
};
|
|
@@ -25,30 +21,33 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
21
|
value: mod,
|
|
26
22
|
enumerable: true
|
|
27
23
|
}) : target, mod));
|
|
28
|
-
|
|
29
24
|
//#endregion
|
|
30
25
|
let colorette = require("colorette");
|
|
31
26
|
let pgsql_deparser = require("pgsql-deparser");
|
|
32
27
|
let zod = require("zod");
|
|
33
28
|
let dedent = require("dedent");
|
|
34
29
|
dedent = __toESM(dedent);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function is$1(node, kind) {
|
|
30
|
+
//#region src/sql/ast-utils.ts
|
|
31
|
+
function is(node, kind) {
|
|
38
32
|
return kind in node;
|
|
39
33
|
}
|
|
40
|
-
function isANode
|
|
34
|
+
function isANode(node) {
|
|
41
35
|
if (typeof node !== "object" || node === null) return false;
|
|
42
36
|
const keys = Object.keys(node);
|
|
43
37
|
return keys.length === 1 && /^[A-Z]/.test(keys[0]);
|
|
44
38
|
}
|
|
39
|
+
function getNodeKind(node) {
|
|
40
|
+
return Object.keys(node)[0];
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/sql/nudges.ts
|
|
45
44
|
const findFuncCallsOnColumns = (whereClause) => {
|
|
46
45
|
const nudges = [];
|
|
47
46
|
Walker.shallowMatch(whereClause, "FuncCall", (node) => {
|
|
48
47
|
if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) nudges.push({
|
|
49
48
|
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
50
49
|
severity: "WARNING",
|
|
51
|
-
message: "Avoid using functions on columns in
|
|
50
|
+
message: "Avoid using functions on columns in conditions — prevents index usage",
|
|
52
51
|
location: node.FuncCall.location
|
|
53
52
|
});
|
|
54
53
|
});
|
|
@@ -56,10 +55,18 @@ const findFuncCallsOnColumns = (whereClause) => {
|
|
|
56
55
|
if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) nudges.push({
|
|
57
56
|
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
58
57
|
severity: "WARNING",
|
|
59
|
-
message: "Avoid using functions on columns in
|
|
58
|
+
message: "Avoid using functions on columns in conditions — prevents index usage",
|
|
60
59
|
location: node.CoalesceExpr.location
|
|
61
60
|
});
|
|
62
61
|
});
|
|
62
|
+
Walker.shallowMatch(whereClause, "TypeCast", (node) => {
|
|
63
|
+
if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) nudges.push({
|
|
64
|
+
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
65
|
+
severity: "WARNING",
|
|
66
|
+
message: "Avoid using functions on columns in WHERE clause",
|
|
67
|
+
location: node.TypeCast.location
|
|
68
|
+
});
|
|
69
|
+
});
|
|
63
70
|
return nudges;
|
|
64
71
|
};
|
|
65
72
|
/**
|
|
@@ -68,38 +75,38 @@ const findFuncCallsOnColumns = (whereClause) => {
|
|
|
68
75
|
*/
|
|
69
76
|
function parseNudges(node, stack) {
|
|
70
77
|
const nudges = [];
|
|
71
|
-
if (is
|
|
78
|
+
if (is(node, "SelectStmt")) {
|
|
72
79
|
if (node.SelectStmt.whereClause) nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));
|
|
73
80
|
const star = node.SelectStmt.targetList?.find((target) => {
|
|
74
|
-
if (!(is
|
|
81
|
+
if (!(is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "ColumnRef"))) return false;
|
|
75
82
|
const fields = target.ResTarget.val.ColumnRef.fields;
|
|
76
|
-
if (!fields?.some((field) => is
|
|
83
|
+
if (!fields?.some((field) => is(field, "A_Star"))) return false;
|
|
77
84
|
if (fields.length > 1) return false;
|
|
78
85
|
return true;
|
|
79
86
|
});
|
|
80
87
|
if (star) {
|
|
81
88
|
const fromClause = node.SelectStmt.fromClause;
|
|
82
|
-
if (!(fromClause && fromClause.length > 0 && fromClause.every((item) => is
|
|
89
|
+
if (!(fromClause && fromClause.length > 0 && fromClause.every((item) => is(item, "RangeSubselect")))) nudges.push({
|
|
83
90
|
kind: "AVOID_SELECT_STAR",
|
|
84
91
|
severity: "INFO",
|
|
85
92
|
message: "Avoid using SELECT *",
|
|
86
93
|
location: star.ResTarget.location
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
|
-
for (const target of node.SelectStmt.targetList ?? []) if (is
|
|
96
|
+
for (const target of node.SelectStmt.targetList ?? []) if (is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "SubLink") && target.ResTarget.val.SubLink.subLinkType === "EXPR_SUBLINK") nudges.push({
|
|
90
97
|
kind: "AVOID_SCALAR_SUBQUERY_IN_SELECT",
|
|
91
98
|
severity: "WARNING",
|
|
92
99
|
message: "Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN",
|
|
93
100
|
location: target.ResTarget.val.SubLink.location
|
|
94
101
|
});
|
|
95
102
|
}
|
|
96
|
-
if (is
|
|
103
|
+
if (is(node, "SelectStmt")) {
|
|
97
104
|
if (!stack.some((item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr")) {
|
|
98
105
|
if (node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0) {
|
|
99
106
|
if (node.SelectStmt.fromClause.some((fromItem) => {
|
|
100
|
-
return is
|
|
107
|
+
return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
|
|
101
108
|
})) {
|
|
102
|
-
const fromLocation = node.SelectStmt.fromClause.find((item) => is
|
|
109
|
+
const fromLocation = node.SelectStmt.fromClause.find((item) => is(item, "RangeVar"))?.RangeVar.location;
|
|
103
110
|
if (!node.SelectStmt.whereClause) nudges.push({
|
|
104
111
|
kind: "MISSING_WHERE_CLAUSE",
|
|
105
112
|
severity: "INFO",
|
|
@@ -116,12 +123,12 @@ function parseNudges(node, stack) {
|
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
|
-
if (is
|
|
120
|
-
if (!is
|
|
126
|
+
if (is(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
|
|
127
|
+
if (!is(sortItem, "SortBy")) continue;
|
|
121
128
|
const sortDir = sortItem.SortBy.sortby_dir ?? "SORTBY_DEFAULT";
|
|
122
129
|
const sortNulls = sortItem.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT";
|
|
123
130
|
if (sortDir === "SORTBY_DESC" && sortNulls === "SORTBY_NULLS_DEFAULT") {
|
|
124
|
-
if (sortItem.SortBy.node && is
|
|
131
|
+
if (sortItem.SortBy.node && is(sortItem.SortBy.node, "ColumnRef")) {
|
|
125
132
|
const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);
|
|
126
133
|
if (!(sortColumnName !== null && whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName))) nudges.push({
|
|
127
134
|
kind: "NULLS_FIRST_IN_DESC_ORDER",
|
|
@@ -132,8 +139,8 @@ function parseNudges(node, stack) {
|
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
}
|
|
135
|
-
if (is
|
|
136
|
-
if (node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is
|
|
142
|
+
if (is(node, "A_Expr")) {
|
|
143
|
+
if (node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is(node.A_Expr.name[0], "String") && (node.A_Expr.name[0].String.sval === "=" || node.A_Expr.name[0].String.sval === "!=" || node.A_Expr.name[0].String.sval === "<>")) {
|
|
137
144
|
const leftIsNull = isNullConstant(node.A_Expr.lexpr);
|
|
138
145
|
const rightIsNull = isNullConstant(node.A_Expr.rexpr);
|
|
139
146
|
if (leftIsNull || rightIsNull) nudges.push({
|
|
@@ -147,7 +154,7 @@ function parseNudges(node, stack) {
|
|
|
147
154
|
const patternString = getStringConstantValue(node.A_Expr.rexpr);
|
|
148
155
|
if (patternString && patternString.startsWith("%")) {
|
|
149
156
|
let stringNode;
|
|
150
|
-
if (is
|
|
157
|
+
if (is(node.A_Expr.rexpr, "A_Const")) stringNode = node.A_Expr.rexpr.A_Const;
|
|
151
158
|
nudges.push({
|
|
152
159
|
kind: "AVOID_LEADING_WILDCARD_LIKE",
|
|
153
160
|
severity: "WARNING",
|
|
@@ -157,48 +164,52 @@ function parseNudges(node, stack) {
|
|
|
157
164
|
}
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
|
-
if (is
|
|
161
|
-
for (const sortItem of node.SelectStmt.sortClause) if (is
|
|
167
|
+
if (is(node, "SelectStmt") && node.SelectStmt.sortClause) {
|
|
168
|
+
for (const sortItem of node.SelectStmt.sortClause) if (is(sortItem, "SortBy") && sortItem.SortBy.node && is(sortItem.SortBy.node, "FuncCall") && sortItem.SortBy.node.FuncCall.funcname?.some((name) => is(name, "String") && name.String.sval === "random")) nudges.push({
|
|
162
169
|
kind: "AVOID_ORDER_BY_RANDOM",
|
|
163
170
|
severity: "WARNING",
|
|
164
171
|
message: "Avoid using ORDER BY random()",
|
|
165
172
|
location: sortItem.SortBy.node.FuncCall.location
|
|
166
173
|
});
|
|
167
174
|
}
|
|
168
|
-
if (is
|
|
175
|
+
if (is(node, "SelectStmt") && node.SelectStmt.distinctClause) nudges.push({
|
|
169
176
|
kind: "AVOID_DISTINCT_WITHOUT_REASON",
|
|
170
177
|
severity: "WARNING",
|
|
171
178
|
message: "Avoid using DISTINCT without a reason"
|
|
172
179
|
});
|
|
173
|
-
if (is
|
|
174
|
-
if (
|
|
180
|
+
if (is(node, "JoinExpr")) {
|
|
181
|
+
if (node.JoinExpr.quals) nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));
|
|
182
|
+
else if (!node.JoinExpr.usingClause) nudges.push({
|
|
175
183
|
kind: "MISSING_JOIN_CONDITION",
|
|
176
184
|
severity: "WARNING",
|
|
177
185
|
message: "Missing JOIN condition"
|
|
178
186
|
});
|
|
179
187
|
}
|
|
180
|
-
if (is
|
|
181
|
-
const tables = node.SelectStmt.fromClause.filter((item) => is
|
|
182
|
-
if (tables.length > 1)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
|
|
189
|
+
const tables = node.SelectStmt.fromClause.filter((item) => is(item, "RangeVar"));
|
|
190
|
+
if (tables.length > 1) {
|
|
191
|
+
const tableNames = new Set(tables.map((t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || ""));
|
|
192
|
+
if (!whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)) nudges.push({
|
|
193
|
+
kind: "MISSING_JOIN_CONDITION",
|
|
194
|
+
severity: "WARNING",
|
|
195
|
+
message: "Missing JOIN condition",
|
|
196
|
+
location: tables[1].RangeVar.location
|
|
197
|
+
});
|
|
198
|
+
}
|
|
188
199
|
}
|
|
189
|
-
if (is
|
|
190
|
-
if (countBoolOrConditions(node) >= 3) nudges.push({
|
|
200
|
+
if (is(node, "BoolExpr") && node.BoolExpr.boolop === "OR_EXPR") {
|
|
201
|
+
if (countBoolOrConditions(node) >= 3 && allOrBranchesReferenceSameColumn(node)) nudges.push({
|
|
191
202
|
kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
|
|
192
203
|
severity: "WARNING",
|
|
193
204
|
message: "Consider using IN instead of many ORs",
|
|
194
205
|
location: node.BoolExpr.location
|
|
195
206
|
});
|
|
196
207
|
}
|
|
197
|
-
if (is
|
|
208
|
+
if (is(node, "BoolExpr") && node.BoolExpr.boolop === "NOT_EXPR") {
|
|
198
209
|
const args = node.BoolExpr.args;
|
|
199
210
|
if (args && args.length === 1) {
|
|
200
211
|
const inner = args[0];
|
|
201
|
-
if (isANode
|
|
212
|
+
if (isANode(inner) && is(inner, "SubLink") && inner.SubLink.subLinkType === "ANY_SUBLINK") nudges.push({
|
|
202
213
|
kind: "PREFER_NOT_EXISTS_OVER_NOT_IN",
|
|
203
214
|
severity: "WARNING",
|
|
204
215
|
message: "Prefer NOT EXISTS over NOT IN (SELECT ...)",
|
|
@@ -206,21 +217,21 @@ function parseNudges(node, stack) {
|
|
|
206
217
|
});
|
|
207
218
|
}
|
|
208
219
|
}
|
|
209
|
-
if (is
|
|
220
|
+
if (is(node, "FuncCall")) {
|
|
210
221
|
const funcName = node.FuncCall.funcname;
|
|
211
|
-
if (funcName && funcName.length === 1 && is
|
|
222
|
+
if (funcName && funcName.length === 1 && is(funcName[0], "String") && funcName[0].String.sval === "count" && node.FuncCall.args && !node.FuncCall.agg_star && !node.FuncCall.agg_distinct) nudges.push({
|
|
212
223
|
kind: "PREFER_COUNT_STAR_OVER_COUNT_COLUMN",
|
|
213
224
|
severity: "INFO",
|
|
214
225
|
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.",
|
|
215
226
|
location: node.FuncCall.location
|
|
216
227
|
});
|
|
217
228
|
}
|
|
218
|
-
if (is
|
|
229
|
+
if (is(node, "SelectStmt") && node.SelectStmt.havingClause) {
|
|
219
230
|
if (!containsAggregate(node.SelectStmt.havingClause)) {
|
|
220
231
|
const having = node.SelectStmt.havingClause;
|
|
221
232
|
let location;
|
|
222
|
-
if (is
|
|
223
|
-
else if (is
|
|
233
|
+
if (is(having, "A_Expr")) location = having.A_Expr.location;
|
|
234
|
+
else if (is(having, "BoolExpr")) location = having.BoolExpr.location;
|
|
224
235
|
nudges.push({
|
|
225
236
|
kind: "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES",
|
|
226
237
|
severity: "INFO",
|
|
@@ -229,7 +240,7 @@ function parseNudges(node, stack) {
|
|
|
229
240
|
});
|
|
230
241
|
}
|
|
231
242
|
}
|
|
232
|
-
if (is
|
|
243
|
+
if (is(node, "FuncCall")) {
|
|
233
244
|
if (stack.some((item) => item === "whereClause") && node.FuncCall.args) {
|
|
234
245
|
const name = getFuncName(node);
|
|
235
246
|
if (name && JSONB_SET_RETURNING_FUNCTIONS.has(name) && containsColumnRef(node.FuncCall.args)) nudges.push({
|
|
@@ -240,11 +251,11 @@ function parseNudges(node, stack) {
|
|
|
240
251
|
});
|
|
241
252
|
}
|
|
242
253
|
}
|
|
243
|
-
if (is
|
|
254
|
+
if (is(node, "A_Expr")) {
|
|
244
255
|
if (node.A_Expr.kind === "AEXPR_IN") {
|
|
245
256
|
let list;
|
|
246
|
-
if (node.A_Expr.lexpr && is
|
|
247
|
-
else if (node.A_Expr.rexpr && is
|
|
257
|
+
if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "List")) list = node.A_Expr.lexpr.List;
|
|
258
|
+
else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "List")) list = node.A_Expr.rexpr.List;
|
|
248
259
|
if (list?.items && list.items.length >= 10) nudges.push({
|
|
249
260
|
kind: "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY",
|
|
250
261
|
message: "`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter",
|
|
@@ -253,8 +264,8 @@ function parseNudges(node, stack) {
|
|
|
253
264
|
});
|
|
254
265
|
}
|
|
255
266
|
}
|
|
256
|
-
if (is
|
|
257
|
-
const funcname = node.FuncCall.funcname?.[0] && is
|
|
267
|
+
if (is(node, "FuncCall")) {
|
|
268
|
+
const funcname = node.FuncCall.funcname?.[0] && is(node.FuncCall.funcname[0], "String") && node.FuncCall.funcname[0].String.sval;
|
|
258
269
|
if (funcname && [
|
|
259
270
|
"sum",
|
|
260
271
|
"count",
|
|
@@ -263,11 +274,11 @@ function parseNudges(node, stack) {
|
|
|
263
274
|
"max"
|
|
264
275
|
].includes(funcname.toLowerCase())) {
|
|
265
276
|
const firstArg = node.FuncCall.args?.[0];
|
|
266
|
-
if (firstArg && isANode
|
|
277
|
+
if (firstArg && isANode(firstArg) && is(firstArg, "CaseExpr")) {
|
|
267
278
|
const caseExpr = firstArg.CaseExpr;
|
|
268
279
|
if (caseExpr.args && caseExpr.args.length === 1) {
|
|
269
280
|
const defresult = caseExpr.defresult;
|
|
270
|
-
if (!defresult || isANode
|
|
281
|
+
if (!defresult || isANode(defresult) && is(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({
|
|
271
282
|
kind: "PREFER_FILTER_OVER_CASE_IN_AGGREGATE",
|
|
272
283
|
severity: "INFO",
|
|
273
284
|
message: "Use FILTER (WHERE ...) instead of CASE inside aggregate functions",
|
|
@@ -277,14 +288,14 @@ function parseNudges(node, stack) {
|
|
|
277
288
|
}
|
|
278
289
|
}
|
|
279
290
|
}
|
|
280
|
-
if (is
|
|
291
|
+
if (is(node, "SelectStmt") && node.SelectStmt.op === "SETOP_UNION" && !node.SelectStmt.all) nudges.push({
|
|
281
292
|
kind: "PREFER_UNION_ALL_OVER_UNION",
|
|
282
293
|
severity: "INFO",
|
|
283
294
|
message: "UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed"
|
|
284
295
|
});
|
|
285
|
-
if (is
|
|
296
|
+
if (is(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0) {
|
|
286
297
|
const opNode = node.A_Expr.name[0];
|
|
287
|
-
const op = is
|
|
298
|
+
const op = is(opNode, "String") ? opNode.String.sval : null;
|
|
288
299
|
if (op && isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)) nudges.push({
|
|
289
300
|
kind: "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK",
|
|
290
301
|
severity: "INFO",
|
|
@@ -299,32 +310,32 @@ function containsColumnRef(args) {
|
|
|
299
310
|
return false;
|
|
300
311
|
}
|
|
301
312
|
function hasColumnRefInNode(node) {
|
|
302
|
-
if (isANode
|
|
313
|
+
if (isANode(node) && is(node, "ColumnRef")) return true;
|
|
303
314
|
if (typeof node !== "object" || node === null) return false;
|
|
304
315
|
if (Array.isArray(node)) return node.some((item) => hasColumnRefInNode(item));
|
|
305
|
-
if (isANode
|
|
316
|
+
if (isANode(node)) return hasColumnRefInNode(node[Object.keys(node)[0]]);
|
|
306
317
|
for (const child of Object.values(node)) if (hasColumnRefInNode(child)) return true;
|
|
307
318
|
return false;
|
|
308
319
|
}
|
|
309
320
|
function hasActualTablesInJoin(joinExpr) {
|
|
310
|
-
if (joinExpr.JoinExpr.larg && is
|
|
311
|
-
if (joinExpr.JoinExpr.larg && is
|
|
321
|
+
if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "RangeVar")) return true;
|
|
322
|
+
if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "JoinExpr")) {
|
|
312
323
|
if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) return true;
|
|
313
324
|
}
|
|
314
|
-
if (joinExpr.JoinExpr.rarg && is
|
|
315
|
-
if (joinExpr.JoinExpr.rarg && is
|
|
325
|
+
if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "RangeVar")) return true;
|
|
326
|
+
if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "JoinExpr")) {
|
|
316
327
|
if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) return true;
|
|
317
328
|
}
|
|
318
329
|
return false;
|
|
319
330
|
}
|
|
320
331
|
function isNullConstant(node) {
|
|
321
332
|
if (!node || typeof node !== "object") return false;
|
|
322
|
-
if (isANode
|
|
333
|
+
if (isANode(node) && is(node, "A_Const")) return node.A_Const.isnull !== void 0;
|
|
323
334
|
return false;
|
|
324
335
|
}
|
|
325
336
|
function getStringConstantValue(node) {
|
|
326
337
|
if (!node || typeof node !== "object") return null;
|
|
327
|
-
if (isANode
|
|
338
|
+
if (isANode(node) && is(node, "A_Const") && node.A_Const.sval) return node.A_Const.sval.sval || null;
|
|
328
339
|
return null;
|
|
329
340
|
}
|
|
330
341
|
const JSONB_SET_RETURNING_FUNCTIONS = new Set([
|
|
@@ -341,21 +352,21 @@ function getFuncName(node) {
|
|
|
341
352
|
const names = node.FuncCall.funcname;
|
|
342
353
|
if (!names || names.length === 0) return null;
|
|
343
354
|
const last = names[names.length - 1];
|
|
344
|
-
if (isANode
|
|
355
|
+
if (isANode(last) && is(last, "String") && last.String.sval) return last.String.sval;
|
|
345
356
|
return null;
|
|
346
357
|
}
|
|
347
358
|
function getLastColumnRefField(columnRef) {
|
|
348
359
|
const fields = columnRef.ColumnRef.fields;
|
|
349
360
|
if (!fields || fields.length === 0) return null;
|
|
350
361
|
const lastField = fields[fields.length - 1];
|
|
351
|
-
if (isANode
|
|
362
|
+
if (isANode(lastField) && is(lastField, "String")) return lastField.String.sval || null;
|
|
352
363
|
return null;
|
|
353
364
|
}
|
|
354
365
|
function whereHasIsNotNull(whereClause, columnName) {
|
|
355
366
|
if (!whereClause) return false;
|
|
356
367
|
let found = false;
|
|
357
368
|
Walker.shallowMatch(whereClause, "NullTest", (node) => {
|
|
358
|
-
if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is
|
|
369
|
+
if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is(node.NullTest.arg, "ColumnRef")) {
|
|
359
370
|
if (getLastColumnRefField(node.NullTest.arg) === columnName) found = true;
|
|
360
371
|
}
|
|
361
372
|
});
|
|
@@ -376,41 +387,106 @@ const AGGREGATE_FUNCTIONS = new Set([
|
|
|
376
387
|
function containsAggregate(node) {
|
|
377
388
|
if (!node || typeof node !== "object") return false;
|
|
378
389
|
if (Array.isArray(node)) return node.some(containsAggregate);
|
|
379
|
-
if (isANode
|
|
390
|
+
if (isANode(node) && is(node, "FuncCall")) {
|
|
380
391
|
const funcname = node.FuncCall.funcname;
|
|
381
392
|
if (funcname) {
|
|
382
|
-
for (const f of funcname) if (isANode
|
|
393
|
+
for (const f of funcname) if (isANode(f) && is(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
|
|
383
394
|
}
|
|
384
395
|
}
|
|
385
|
-
if (isANode
|
|
396
|
+
if (isANode(node)) return containsAggregate(node[Object.keys(node)[0]]);
|
|
386
397
|
for (const child of Object.values(node)) if (containsAggregate(child)) return true;
|
|
387
398
|
return false;
|
|
388
399
|
}
|
|
389
400
|
function countBoolOrConditions(node) {
|
|
390
401
|
if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return 1;
|
|
391
402
|
let count = 0;
|
|
392
|
-
for (const arg of node.BoolExpr.args) if (isANode
|
|
403
|
+
for (const arg of node.BoolExpr.args) if (isANode(arg) && is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") count += countBoolOrConditions(arg);
|
|
393
404
|
else count += 1;
|
|
394
405
|
return count;
|
|
395
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Check whether every leaf of a top-level OR expression references the same
|
|
409
|
+
* left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).
|
|
410
|
+
* Returns false when ORs span different columns — IN rewrite doesn't apply.
|
|
411
|
+
*/
|
|
412
|
+
function allOrBranchesReferenceSameColumn(node) {
|
|
413
|
+
const columns = collectOrLeafColumns(node);
|
|
414
|
+
if (columns.length === 0) return false;
|
|
415
|
+
return columns.every((col) => col === columns[0]);
|
|
416
|
+
}
|
|
417
|
+
function collectOrLeafColumns(node) {
|
|
418
|
+
if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return [];
|
|
419
|
+
const columns = [];
|
|
420
|
+
for (const arg of node.BoolExpr.args) {
|
|
421
|
+
if (!isANode(arg)) continue;
|
|
422
|
+
if (is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") columns.push(...collectOrLeafColumns(arg));
|
|
423
|
+
else if (is(arg, "A_Expr")) {
|
|
424
|
+
const col = getLeftColumnKey(arg);
|
|
425
|
+
if (col) columns.push(col);
|
|
426
|
+
else return [];
|
|
427
|
+
} else return [];
|
|
428
|
+
}
|
|
429
|
+
return columns;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Get a string key for the left-hand column of an A_Expr equality comparison.
|
|
433
|
+
* For `t1.col = value` returns `"t1.col"`, for `col = value` returns `"col"`.
|
|
434
|
+
*/
|
|
435
|
+
function getLeftColumnKey(expr) {
|
|
436
|
+
if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;
|
|
437
|
+
if (!is(expr.A_Expr.lexpr, "ColumnRef")) return null;
|
|
438
|
+
const fields = expr.A_Expr.lexpr.ColumnRef.fields;
|
|
439
|
+
if (!fields) return null;
|
|
440
|
+
return fields.filter((f) => isANode(f) && is(f, "String")).map((f) => f.String.sval).join(".");
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Check if a WHERE clause contains an equality between columns from different
|
|
444
|
+
* tables (indicating an old-style implicit join condition).
|
|
445
|
+
*/
|
|
446
|
+
function whereHasCrossTableEquality(whereClause, tableNames) {
|
|
447
|
+
if (!whereClause) return false;
|
|
448
|
+
if (isANode(whereClause) && is(whereClause, "A_Expr")) {
|
|
449
|
+
if (whereClause.A_Expr.kind === "AEXPR_OP" && whereClause.A_Expr.name?.some((n) => is(n, "String") && n.String.sval === "=")) {
|
|
450
|
+
const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);
|
|
451
|
+
const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);
|
|
452
|
+
if (leftTable && rightTable && leftTable !== rightTable && tableNames.has(leftTable) && tableNames.has(rightTable)) return true;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (isANode(whereClause) && is(whereClause, "BoolExpr")) {
|
|
456
|
+
for (const arg of whereClause.BoolExpr.args ?? []) if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) return true;
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Extract the table qualifier (first field) from a ColumnRef node.
|
|
462
|
+
* e.g. `t1.uid` → `"t1"`, `uid` → null
|
|
463
|
+
*/
|
|
464
|
+
function getColumnRefTableQualifier(node) {
|
|
465
|
+
if (!node || !isANode(node) || !is(node, "ColumnRef")) return null;
|
|
466
|
+
const fields = node.ColumnRef.fields;
|
|
467
|
+
if (!fields || fields.length < 2) return null;
|
|
468
|
+
const first = fields[0];
|
|
469
|
+
if (isANode(first) && is(first, "String")) return first.String.sval || null;
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
396
472
|
function isCountFuncCall(node) {
|
|
397
473
|
if (!node || typeof node !== "object") return false;
|
|
398
|
-
if (!isANode
|
|
474
|
+
if (!isANode(node) || !is(node, "FuncCall")) return false;
|
|
399
475
|
const fc = node.FuncCall;
|
|
400
|
-
if (!(fc.funcname?.some((n) => is
|
|
476
|
+
if (!(fc.funcname?.some((n) => is(n, "String") && n.String.sval === "count") ?? false)) return false;
|
|
401
477
|
if (fc.agg_star) return true;
|
|
402
|
-
if (fc.args && fc.args.length === 1 && isANode
|
|
478
|
+
if (fc.args && fc.args.length === 1 && isANode(fc.args[0]) && is(fc.args[0], "A_Const")) return true;
|
|
403
479
|
return false;
|
|
404
480
|
}
|
|
405
481
|
function isSubLinkWithCount(node) {
|
|
406
482
|
if (!node || typeof node !== "object") return false;
|
|
407
|
-
if (!isANode
|
|
483
|
+
if (!isANode(node) || !is(node, "SubLink")) return false;
|
|
408
484
|
const subselect = node.SubLink.subselect;
|
|
409
|
-
if (!subselect || !isANode
|
|
485
|
+
if (!subselect || !isANode(subselect) || !is(subselect, "SelectStmt")) return false;
|
|
410
486
|
const targets = subselect.SelectStmt.targetList;
|
|
411
487
|
if (!targets || targets.length !== 1) return false;
|
|
412
488
|
const target = targets[0];
|
|
413
|
-
if (!isANode
|
|
489
|
+
if (!isANode(target) || !is(target, "ResTarget") || !target.ResTarget.val) return false;
|
|
414
490
|
return isCountFuncCall(target.ResTarget.val);
|
|
415
491
|
}
|
|
416
492
|
function isCountExpression(node) {
|
|
@@ -418,7 +494,7 @@ function isCountExpression(node) {
|
|
|
418
494
|
}
|
|
419
495
|
function getIntegerConstantValue(node) {
|
|
420
496
|
if (!node || typeof node !== "object") return null;
|
|
421
|
-
if (!isANode
|
|
497
|
+
if (!isANode(node) || !is(node, "A_Const")) return null;
|
|
422
498
|
if (node.A_Const.ival === void 0) return null;
|
|
423
499
|
return node.A_Const.ival.ival ?? 0;
|
|
424
500
|
}
|
|
@@ -441,9 +517,8 @@ function isExistenceCheckPattern(lexpr, rexpr, op) {
|
|
|
441
517
|
}
|
|
442
518
|
return false;
|
|
443
519
|
}
|
|
444
|
-
|
|
445
520
|
//#endregion
|
|
446
|
-
//#region \0@oxc-project+runtime@0.
|
|
521
|
+
//#region \0@oxc-project+runtime@0.122.0/helpers/typeof.js
|
|
447
522
|
function _typeof(o) {
|
|
448
523
|
"@babel/helpers - typeof";
|
|
449
524
|
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
@@ -452,9 +527,8 @@ function _typeof(o) {
|
|
|
452
527
|
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
453
528
|
}, _typeof(o);
|
|
454
529
|
}
|
|
455
|
-
|
|
456
530
|
//#endregion
|
|
457
|
-
//#region \0@oxc-project+runtime@0.
|
|
531
|
+
//#region \0@oxc-project+runtime@0.122.0/helpers/toPrimitive.js
|
|
458
532
|
function toPrimitive(t, r) {
|
|
459
533
|
if ("object" != _typeof(t) || !t) return t;
|
|
460
534
|
var e = t[Symbol.toPrimitive];
|
|
@@ -465,16 +539,14 @@ function toPrimitive(t, r) {
|
|
|
465
539
|
}
|
|
466
540
|
return ("string" === r ? String : Number)(t);
|
|
467
541
|
}
|
|
468
|
-
|
|
469
542
|
//#endregion
|
|
470
|
-
//#region \0@oxc-project+runtime@0.
|
|
543
|
+
//#region \0@oxc-project+runtime@0.122.0/helpers/toPropertyKey.js
|
|
471
544
|
function toPropertyKey(t) {
|
|
472
545
|
var i = toPrimitive(t, "string");
|
|
473
546
|
return "symbol" == _typeof(i) ? i : i + "";
|
|
474
547
|
}
|
|
475
|
-
|
|
476
548
|
//#endregion
|
|
477
|
-
//#region \0@oxc-project+runtime@0.
|
|
549
|
+
//#region \0@oxc-project+runtime@0.122.0/helpers/defineProperty.js
|
|
478
550
|
function _defineProperty(e, r, t) {
|
|
479
551
|
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
480
552
|
value: t,
|
|
@@ -483,7 +555,6 @@ function _defineProperty(e, r, t) {
|
|
|
483
555
|
writable: !0
|
|
484
556
|
}) : e[r] = t, e;
|
|
485
557
|
}
|
|
486
|
-
|
|
487
558
|
//#endregion
|
|
488
559
|
//#region src/sql/walker.ts
|
|
489
560
|
const JSONB_EXTRACTION_OPS = new Set(["->", "->>"]);
|
|
@@ -730,17 +801,6 @@ var Walker = class Walker {
|
|
|
730
801
|
} else for (const [key, child] of Object.entries(node)) Walker.doTraverse(child, [...stack, key], callback);
|
|
731
802
|
}
|
|
732
803
|
};
|
|
733
|
-
function is(node, kind) {
|
|
734
|
-
return kind in node;
|
|
735
|
-
}
|
|
736
|
-
function getNodeKind(node) {
|
|
737
|
-
return Object.keys(node)[0];
|
|
738
|
-
}
|
|
739
|
-
function isANode(node) {
|
|
740
|
-
if (typeof node !== "object" || node === null) return false;
|
|
741
|
-
const keys = Object.keys(node);
|
|
742
|
-
return keys.length === 1 && /^[A-Z]/.test(keys[0]);
|
|
743
|
-
}
|
|
744
804
|
/**
|
|
745
805
|
* Given an operand of a comparison (e.g. the left side of `=`), check whether
|
|
746
806
|
* it is a JSONB path extraction expression such as `data->>'email'` or
|
|
@@ -792,7 +852,6 @@ function stripTableQualifiers(node) {
|
|
|
792
852
|
for (const value of Object.values(node)) if (Array.isArray(value)) for (const item of value) stripTableQualifiers(item);
|
|
793
853
|
else if (typeof value === "object" && value !== null) stripTableQualifiers(value);
|
|
794
854
|
}
|
|
795
|
-
|
|
796
855
|
//#endregion
|
|
797
856
|
//#region src/sql/analyzer.ts
|
|
798
857
|
const ignoredIdentifier = "__qd_placeholder";
|
|
@@ -1018,10 +1077,17 @@ var Analyzer = class {
|
|
|
1018
1077
|
};
|
|
1019
1078
|
}
|
|
1020
1079
|
};
|
|
1021
|
-
|
|
1022
1080
|
//#endregion
|
|
1023
1081
|
//#region src/sql/database.ts
|
|
1024
1082
|
const PostgresVersion = zod.z.string().brand("PostgresVersion");
|
|
1083
|
+
/** Zod schema for PostgresExplainStage — validates common fields, passes through variant-specific ones. */
|
|
1084
|
+
const PostgresExplainStageSchema = zod.z.lazy(() => zod.z.object({
|
|
1085
|
+
"Node Type": zod.z.string(),
|
|
1086
|
+
"Total Cost": zod.z.number(),
|
|
1087
|
+
"Plan Width": zod.z.number(),
|
|
1088
|
+
"Node Id": zod.z.number().optional(),
|
|
1089
|
+
Plans: zod.z.array(PostgresExplainStageSchema).optional()
|
|
1090
|
+
}).passthrough());
|
|
1025
1091
|
/**
|
|
1026
1092
|
* Drops a disabled index. Rollsback if it fails for any reason
|
|
1027
1093
|
* @returns Did dropping the index succeed?
|
|
@@ -1038,7 +1104,6 @@ async function dropIndex(tx, index) {
|
|
|
1038
1104
|
return false;
|
|
1039
1105
|
}
|
|
1040
1106
|
}
|
|
1041
|
-
|
|
1042
1107
|
//#endregion
|
|
1043
1108
|
//#region src/sql/indexes.ts
|
|
1044
1109
|
function isIndexSupported(index) {
|
|
@@ -1051,7 +1116,6 @@ function isIndexSupported(index) {
|
|
|
1051
1116
|
function isIndexProbablyDroppable(index) {
|
|
1052
1117
|
return !index.is_primary && !index.is_unique;
|
|
1053
1118
|
}
|
|
1054
|
-
|
|
1055
1119
|
//#endregion
|
|
1056
1120
|
//#region src/sql/builder.ts
|
|
1057
1121
|
var PostgresQueryBuilder = class PostgresQueryBuilder {
|
|
@@ -1141,7 +1205,6 @@ var PostgresQueryBuilder = class PostgresQueryBuilder {
|
|
|
1141
1205
|
return query;
|
|
1142
1206
|
}
|
|
1143
1207
|
};
|
|
1144
|
-
|
|
1145
1208
|
//#endregion
|
|
1146
1209
|
//#region src/sql/pg-identifier.ts
|
|
1147
1210
|
/**
|
|
@@ -1353,7 +1416,6 @@ _defineProperty(PgIdentifier, "reservedKeywords", new Set([
|
|
|
1353
1416
|
"xmlserialize",
|
|
1354
1417
|
"xmltable"
|
|
1355
1418
|
]));
|
|
1356
|
-
|
|
1357
1419
|
//#endregion
|
|
1358
1420
|
//#region src/sql/permutations.ts
|
|
1359
1421
|
/**
|
|
@@ -1378,7 +1440,6 @@ function permutationsWithDescendingLength(arr) {
|
|
|
1378
1440
|
collected.sort((a, b) => b.length - a.length);
|
|
1379
1441
|
return collected;
|
|
1380
1442
|
}
|
|
1381
|
-
|
|
1382
1443
|
//#endregion
|
|
1383
1444
|
//#region src/optimizer/genalgo.ts
|
|
1384
1445
|
var IndexOptimizer = class IndexOptimizer {
|
|
@@ -1717,7 +1778,6 @@ var RollbackError = class {
|
|
|
1717
1778
|
};
|
|
1718
1779
|
const PROCEED = Symbol("PROCEED");
|
|
1719
1780
|
const SKIP = Symbol("SKIP");
|
|
1720
|
-
|
|
1721
1781
|
//#endregion
|
|
1722
1782
|
//#region src/optimizer/statistics.ts
|
|
1723
1783
|
const StatisticsSource = zod.z.union([zod.z.object({
|
|
@@ -2357,7 +2417,6 @@ _defineProperty(Statistics, "defaultStatsMode", Object.freeze({
|
|
|
2357
2417
|
reltuples: DEFAULT_RELTUPLES,
|
|
2358
2418
|
relpages: DEFAULT_RELPAGES
|
|
2359
2419
|
}));
|
|
2360
|
-
|
|
2361
2420
|
//#endregion
|
|
2362
2421
|
//#region src/optimizer/pss-rewriter.ts
|
|
2363
2422
|
/**
|
|
@@ -2384,7 +2443,6 @@ var PssRewriter = class {
|
|
|
2384
2443
|
});
|
|
2385
2444
|
}
|
|
2386
2445
|
};
|
|
2387
|
-
|
|
2388
2446
|
//#endregion
|
|
2389
2447
|
exports.Analyzer = Analyzer;
|
|
2390
2448
|
exports.ExportedStats = ExportedStats;
|
|
@@ -2395,6 +2453,7 @@ exports.ExportedStatsV1 = ExportedStatsV1;
|
|
|
2395
2453
|
exports.IndexOptimizer = IndexOptimizer;
|
|
2396
2454
|
exports.PROCEED = PROCEED;
|
|
2397
2455
|
exports.PgIdentifier = PgIdentifier;
|
|
2456
|
+
exports.PostgresExplainStageSchema = PostgresExplainStageSchema;
|
|
2398
2457
|
exports.PostgresQueryBuilder = PostgresQueryBuilder;
|
|
2399
2458
|
exports.PostgresVersion = PostgresVersion;
|
|
2400
2459
|
exports.PssRewriter = PssRewriter;
|
|
@@ -2407,4 +2466,5 @@ exports.ignoredIdentifier = ignoredIdentifier;
|
|
|
2407
2466
|
exports.isIndexProbablyDroppable = isIndexProbablyDroppable;
|
|
2408
2467
|
exports.isIndexSupported = isIndexSupported;
|
|
2409
2468
|
exports.parseNudges = parseNudges;
|
|
2469
|
+
|
|
2410
2470
|
//# sourceMappingURL=index.cjs.map
|