@query-doctor/core 0.6.0 → 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.mjs
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
import { bgMagentaBright, blue, dim, gray, green, magenta, red, strikethrough, yellow } from "colorette";
|
|
3
3
|
import { deparseSync } from "pgsql-deparser";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import dedent from "dedent";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function is$1(node, kind) {
|
|
6
|
+
//#region src/sql/ast-utils.ts
|
|
7
|
+
function is(node, kind) {
|
|
9
8
|
return kind in node;
|
|
10
9
|
}
|
|
11
|
-
function isANode
|
|
10
|
+
function isANode(node) {
|
|
12
11
|
if (typeof node !== "object" || node === null) return false;
|
|
13
12
|
const keys = Object.keys(node);
|
|
14
13
|
return keys.length === 1 && /^[A-Z]/.test(keys[0]);
|
|
15
14
|
}
|
|
15
|
+
function getNodeKind(node) {
|
|
16
|
+
return Object.keys(node)[0];
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/sql/nudges.ts
|
|
16
20
|
const findFuncCallsOnColumns = (whereClause) => {
|
|
17
21
|
const nudges = [];
|
|
18
22
|
Walker.shallowMatch(whereClause, "FuncCall", (node) => {
|
|
19
23
|
if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) nudges.push({
|
|
20
24
|
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
21
25
|
severity: "WARNING",
|
|
22
|
-
message: "Avoid using functions on columns in
|
|
26
|
+
message: "Avoid using functions on columns in conditions — prevents index usage",
|
|
23
27
|
location: node.FuncCall.location
|
|
24
28
|
});
|
|
25
29
|
});
|
|
@@ -27,10 +31,18 @@ const findFuncCallsOnColumns = (whereClause) => {
|
|
|
27
31
|
if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) nudges.push({
|
|
28
32
|
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
29
33
|
severity: "WARNING",
|
|
30
|
-
message: "Avoid using functions on columns in
|
|
34
|
+
message: "Avoid using functions on columns in conditions — prevents index usage",
|
|
31
35
|
location: node.CoalesceExpr.location
|
|
32
36
|
});
|
|
33
37
|
});
|
|
38
|
+
Walker.shallowMatch(whereClause, "TypeCast", (node) => {
|
|
39
|
+
if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) nudges.push({
|
|
40
|
+
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
41
|
+
severity: "WARNING",
|
|
42
|
+
message: "Avoid using functions on columns in WHERE clause",
|
|
43
|
+
location: node.TypeCast.location
|
|
44
|
+
});
|
|
45
|
+
});
|
|
34
46
|
return nudges;
|
|
35
47
|
};
|
|
36
48
|
/**
|
|
@@ -39,38 +51,38 @@ const findFuncCallsOnColumns = (whereClause) => {
|
|
|
39
51
|
*/
|
|
40
52
|
function parseNudges(node, stack) {
|
|
41
53
|
const nudges = [];
|
|
42
|
-
if (is
|
|
54
|
+
if (is(node, "SelectStmt")) {
|
|
43
55
|
if (node.SelectStmt.whereClause) nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));
|
|
44
56
|
const star = node.SelectStmt.targetList?.find((target) => {
|
|
45
|
-
if (!(is
|
|
57
|
+
if (!(is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "ColumnRef"))) return false;
|
|
46
58
|
const fields = target.ResTarget.val.ColumnRef.fields;
|
|
47
|
-
if (!fields?.some((field) => is
|
|
59
|
+
if (!fields?.some((field) => is(field, "A_Star"))) return false;
|
|
48
60
|
if (fields.length > 1) return false;
|
|
49
61
|
return true;
|
|
50
62
|
});
|
|
51
63
|
if (star) {
|
|
52
64
|
const fromClause = node.SelectStmt.fromClause;
|
|
53
|
-
if (!(fromClause && fromClause.length > 0 && fromClause.every((item) => is
|
|
65
|
+
if (!(fromClause && fromClause.length > 0 && fromClause.every((item) => is(item, "RangeSubselect")))) nudges.push({
|
|
54
66
|
kind: "AVOID_SELECT_STAR",
|
|
55
67
|
severity: "INFO",
|
|
56
68
|
message: "Avoid using SELECT *",
|
|
57
69
|
location: star.ResTarget.location
|
|
58
70
|
});
|
|
59
71
|
}
|
|
60
|
-
for (const target of node.SelectStmt.targetList ?? []) if (is
|
|
72
|
+
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({
|
|
61
73
|
kind: "AVOID_SCALAR_SUBQUERY_IN_SELECT",
|
|
62
74
|
severity: "WARNING",
|
|
63
75
|
message: "Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN",
|
|
64
76
|
location: target.ResTarget.val.SubLink.location
|
|
65
77
|
});
|
|
66
78
|
}
|
|
67
|
-
if (is
|
|
79
|
+
if (is(node, "SelectStmt")) {
|
|
68
80
|
if (!stack.some((item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr")) {
|
|
69
81
|
if (node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0) {
|
|
70
82
|
if (node.SelectStmt.fromClause.some((fromItem) => {
|
|
71
|
-
return is
|
|
83
|
+
return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
|
|
72
84
|
})) {
|
|
73
|
-
const fromLocation = node.SelectStmt.fromClause.find((item) => is
|
|
85
|
+
const fromLocation = node.SelectStmt.fromClause.find((item) => is(item, "RangeVar"))?.RangeVar.location;
|
|
74
86
|
if (!node.SelectStmt.whereClause) nudges.push({
|
|
75
87
|
kind: "MISSING_WHERE_CLAUSE",
|
|
76
88
|
severity: "INFO",
|
|
@@ -87,12 +99,12 @@ function parseNudges(node, stack) {
|
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
|
-
if (is
|
|
91
|
-
if (!is
|
|
102
|
+
if (is(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
|
|
103
|
+
if (!is(sortItem, "SortBy")) continue;
|
|
92
104
|
const sortDir = sortItem.SortBy.sortby_dir ?? "SORTBY_DEFAULT";
|
|
93
105
|
const sortNulls = sortItem.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT";
|
|
94
106
|
if (sortDir === "SORTBY_DESC" && sortNulls === "SORTBY_NULLS_DEFAULT") {
|
|
95
|
-
if (sortItem.SortBy.node && is
|
|
107
|
+
if (sortItem.SortBy.node && is(sortItem.SortBy.node, "ColumnRef")) {
|
|
96
108
|
const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);
|
|
97
109
|
if (!(sortColumnName !== null && whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName))) nudges.push({
|
|
98
110
|
kind: "NULLS_FIRST_IN_DESC_ORDER",
|
|
@@ -103,8 +115,8 @@ function parseNudges(node, stack) {
|
|
|
103
115
|
}
|
|
104
116
|
}
|
|
105
117
|
}
|
|
106
|
-
if (is
|
|
107
|
-
if (node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0 && is
|
|
118
|
+
if (is(node, "A_Expr")) {
|
|
119
|
+
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 === "<>")) {
|
|
108
120
|
const leftIsNull = isNullConstant(node.A_Expr.lexpr);
|
|
109
121
|
const rightIsNull = isNullConstant(node.A_Expr.rexpr);
|
|
110
122
|
if (leftIsNull || rightIsNull) nudges.push({
|
|
@@ -118,7 +130,7 @@ function parseNudges(node, stack) {
|
|
|
118
130
|
const patternString = getStringConstantValue(node.A_Expr.rexpr);
|
|
119
131
|
if (patternString && patternString.startsWith("%")) {
|
|
120
132
|
let stringNode;
|
|
121
|
-
if (is
|
|
133
|
+
if (is(node.A_Expr.rexpr, "A_Const")) stringNode = node.A_Expr.rexpr.A_Const;
|
|
122
134
|
nudges.push({
|
|
123
135
|
kind: "AVOID_LEADING_WILDCARD_LIKE",
|
|
124
136
|
severity: "WARNING",
|
|
@@ -128,48 +140,52 @@ function parseNudges(node, stack) {
|
|
|
128
140
|
}
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
|
-
if (is
|
|
132
|
-
for (const sortItem of node.SelectStmt.sortClause) if (is
|
|
143
|
+
if (is(node, "SelectStmt") && node.SelectStmt.sortClause) {
|
|
144
|
+
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({
|
|
133
145
|
kind: "AVOID_ORDER_BY_RANDOM",
|
|
134
146
|
severity: "WARNING",
|
|
135
147
|
message: "Avoid using ORDER BY random()",
|
|
136
148
|
location: sortItem.SortBy.node.FuncCall.location
|
|
137
149
|
});
|
|
138
150
|
}
|
|
139
|
-
if (is
|
|
151
|
+
if (is(node, "SelectStmt") && node.SelectStmt.distinctClause) nudges.push({
|
|
140
152
|
kind: "AVOID_DISTINCT_WITHOUT_REASON",
|
|
141
153
|
severity: "WARNING",
|
|
142
154
|
message: "Avoid using DISTINCT without a reason"
|
|
143
155
|
});
|
|
144
|
-
if (is
|
|
145
|
-
if (
|
|
156
|
+
if (is(node, "JoinExpr")) {
|
|
157
|
+
if (node.JoinExpr.quals) nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));
|
|
158
|
+
else if (!node.JoinExpr.usingClause) nudges.push({
|
|
146
159
|
kind: "MISSING_JOIN_CONDITION",
|
|
147
160
|
severity: "WARNING",
|
|
148
161
|
message: "Missing JOIN condition"
|
|
149
162
|
});
|
|
150
163
|
}
|
|
151
|
-
if (is
|
|
152
|
-
const tables = node.SelectStmt.fromClause.filter((item) => is
|
|
153
|
-
if (tables.length > 1)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
|
|
165
|
+
const tables = node.SelectStmt.fromClause.filter((item) => is(item, "RangeVar"));
|
|
166
|
+
if (tables.length > 1) {
|
|
167
|
+
const tableNames = new Set(tables.map((t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || ""));
|
|
168
|
+
if (!whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)) nudges.push({
|
|
169
|
+
kind: "MISSING_JOIN_CONDITION",
|
|
170
|
+
severity: "WARNING",
|
|
171
|
+
message: "Missing JOIN condition",
|
|
172
|
+
location: tables[1].RangeVar.location
|
|
173
|
+
});
|
|
174
|
+
}
|
|
159
175
|
}
|
|
160
|
-
if (is
|
|
161
|
-
if (countBoolOrConditions(node) >= 3) nudges.push({
|
|
176
|
+
if (is(node, "BoolExpr") && node.BoolExpr.boolop === "OR_EXPR") {
|
|
177
|
+
if (countBoolOrConditions(node) >= 3 && allOrBranchesReferenceSameColumn(node)) nudges.push({
|
|
162
178
|
kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
|
|
163
179
|
severity: "WARNING",
|
|
164
180
|
message: "Consider using IN instead of many ORs",
|
|
165
181
|
location: node.BoolExpr.location
|
|
166
182
|
});
|
|
167
183
|
}
|
|
168
|
-
if (is
|
|
184
|
+
if (is(node, "BoolExpr") && node.BoolExpr.boolop === "NOT_EXPR") {
|
|
169
185
|
const args = node.BoolExpr.args;
|
|
170
186
|
if (args && args.length === 1) {
|
|
171
187
|
const inner = args[0];
|
|
172
|
-
if (isANode
|
|
188
|
+
if (isANode(inner) && is(inner, "SubLink") && inner.SubLink.subLinkType === "ANY_SUBLINK") nudges.push({
|
|
173
189
|
kind: "PREFER_NOT_EXISTS_OVER_NOT_IN",
|
|
174
190
|
severity: "WARNING",
|
|
175
191
|
message: "Prefer NOT EXISTS over NOT IN (SELECT ...)",
|
|
@@ -177,21 +193,21 @@ function parseNudges(node, stack) {
|
|
|
177
193
|
});
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
|
-
if (is
|
|
196
|
+
if (is(node, "FuncCall")) {
|
|
181
197
|
const funcName = node.FuncCall.funcname;
|
|
182
|
-
if (funcName && funcName.length === 1 && is
|
|
198
|
+
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({
|
|
183
199
|
kind: "PREFER_COUNT_STAR_OVER_COUNT_COLUMN",
|
|
184
200
|
severity: "INFO",
|
|
185
201
|
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.",
|
|
186
202
|
location: node.FuncCall.location
|
|
187
203
|
});
|
|
188
204
|
}
|
|
189
|
-
if (is
|
|
205
|
+
if (is(node, "SelectStmt") && node.SelectStmt.havingClause) {
|
|
190
206
|
if (!containsAggregate(node.SelectStmt.havingClause)) {
|
|
191
207
|
const having = node.SelectStmt.havingClause;
|
|
192
208
|
let location;
|
|
193
|
-
if (is
|
|
194
|
-
else if (is
|
|
209
|
+
if (is(having, "A_Expr")) location = having.A_Expr.location;
|
|
210
|
+
else if (is(having, "BoolExpr")) location = having.BoolExpr.location;
|
|
195
211
|
nudges.push({
|
|
196
212
|
kind: "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES",
|
|
197
213
|
severity: "INFO",
|
|
@@ -200,7 +216,7 @@ function parseNudges(node, stack) {
|
|
|
200
216
|
});
|
|
201
217
|
}
|
|
202
218
|
}
|
|
203
|
-
if (is
|
|
219
|
+
if (is(node, "FuncCall")) {
|
|
204
220
|
if (stack.some((item) => item === "whereClause") && node.FuncCall.args) {
|
|
205
221
|
const name = getFuncName(node);
|
|
206
222
|
if (name && JSONB_SET_RETURNING_FUNCTIONS.has(name) && containsColumnRef(node.FuncCall.args)) nudges.push({
|
|
@@ -211,11 +227,11 @@ function parseNudges(node, stack) {
|
|
|
211
227
|
});
|
|
212
228
|
}
|
|
213
229
|
}
|
|
214
|
-
if (is
|
|
230
|
+
if (is(node, "A_Expr")) {
|
|
215
231
|
if (node.A_Expr.kind === "AEXPR_IN") {
|
|
216
232
|
let list;
|
|
217
|
-
if (node.A_Expr.lexpr && is
|
|
218
|
-
else if (node.A_Expr.rexpr && is
|
|
233
|
+
if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "List")) list = node.A_Expr.lexpr.List;
|
|
234
|
+
else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "List")) list = node.A_Expr.rexpr.List;
|
|
219
235
|
if (list?.items && list.items.length >= 10) nudges.push({
|
|
220
236
|
kind: "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY",
|
|
221
237
|
message: "`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter",
|
|
@@ -224,8 +240,8 @@ function parseNudges(node, stack) {
|
|
|
224
240
|
});
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
|
-
if (is
|
|
228
|
-
const funcname = node.FuncCall.funcname?.[0] && is
|
|
243
|
+
if (is(node, "FuncCall")) {
|
|
244
|
+
const funcname = node.FuncCall.funcname?.[0] && is(node.FuncCall.funcname[0], "String") && node.FuncCall.funcname[0].String.sval;
|
|
229
245
|
if (funcname && [
|
|
230
246
|
"sum",
|
|
231
247
|
"count",
|
|
@@ -234,11 +250,11 @@ function parseNudges(node, stack) {
|
|
|
234
250
|
"max"
|
|
235
251
|
].includes(funcname.toLowerCase())) {
|
|
236
252
|
const firstArg = node.FuncCall.args?.[0];
|
|
237
|
-
if (firstArg && isANode
|
|
253
|
+
if (firstArg && isANode(firstArg) && is(firstArg, "CaseExpr")) {
|
|
238
254
|
const caseExpr = firstArg.CaseExpr;
|
|
239
255
|
if (caseExpr.args && caseExpr.args.length === 1) {
|
|
240
256
|
const defresult = caseExpr.defresult;
|
|
241
|
-
if (!defresult || isANode
|
|
257
|
+
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({
|
|
242
258
|
kind: "PREFER_FILTER_OVER_CASE_IN_AGGREGATE",
|
|
243
259
|
severity: "INFO",
|
|
244
260
|
message: "Use FILTER (WHERE ...) instead of CASE inside aggregate functions",
|
|
@@ -248,14 +264,14 @@ function parseNudges(node, stack) {
|
|
|
248
264
|
}
|
|
249
265
|
}
|
|
250
266
|
}
|
|
251
|
-
if (is
|
|
267
|
+
if (is(node, "SelectStmt") && node.SelectStmt.op === "SETOP_UNION" && !node.SelectStmt.all) nudges.push({
|
|
252
268
|
kind: "PREFER_UNION_ALL_OVER_UNION",
|
|
253
269
|
severity: "INFO",
|
|
254
270
|
message: "UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed"
|
|
255
271
|
});
|
|
256
|
-
if (is
|
|
272
|
+
if (is(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0) {
|
|
257
273
|
const opNode = node.A_Expr.name[0];
|
|
258
|
-
const op = is
|
|
274
|
+
const op = is(opNode, "String") ? opNode.String.sval : null;
|
|
259
275
|
if (op && isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)) nudges.push({
|
|
260
276
|
kind: "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK",
|
|
261
277
|
severity: "INFO",
|
|
@@ -270,32 +286,32 @@ function containsColumnRef(args) {
|
|
|
270
286
|
return false;
|
|
271
287
|
}
|
|
272
288
|
function hasColumnRefInNode(node) {
|
|
273
|
-
if (isANode
|
|
289
|
+
if (isANode(node) && is(node, "ColumnRef")) return true;
|
|
274
290
|
if (typeof node !== "object" || node === null) return false;
|
|
275
291
|
if (Array.isArray(node)) return node.some((item) => hasColumnRefInNode(item));
|
|
276
|
-
if (isANode
|
|
292
|
+
if (isANode(node)) return hasColumnRefInNode(node[Object.keys(node)[0]]);
|
|
277
293
|
for (const child of Object.values(node)) if (hasColumnRefInNode(child)) return true;
|
|
278
294
|
return false;
|
|
279
295
|
}
|
|
280
296
|
function hasActualTablesInJoin(joinExpr) {
|
|
281
|
-
if (joinExpr.JoinExpr.larg && is
|
|
282
|
-
if (joinExpr.JoinExpr.larg && is
|
|
297
|
+
if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "RangeVar")) return true;
|
|
298
|
+
if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "JoinExpr")) {
|
|
283
299
|
if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) return true;
|
|
284
300
|
}
|
|
285
|
-
if (joinExpr.JoinExpr.rarg && is
|
|
286
|
-
if (joinExpr.JoinExpr.rarg && is
|
|
301
|
+
if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "RangeVar")) return true;
|
|
302
|
+
if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "JoinExpr")) {
|
|
287
303
|
if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) return true;
|
|
288
304
|
}
|
|
289
305
|
return false;
|
|
290
306
|
}
|
|
291
307
|
function isNullConstant(node) {
|
|
292
308
|
if (!node || typeof node !== "object") return false;
|
|
293
|
-
if (isANode
|
|
309
|
+
if (isANode(node) && is(node, "A_Const")) return node.A_Const.isnull !== void 0;
|
|
294
310
|
return false;
|
|
295
311
|
}
|
|
296
312
|
function getStringConstantValue(node) {
|
|
297
313
|
if (!node || typeof node !== "object") return null;
|
|
298
|
-
if (isANode
|
|
314
|
+
if (isANode(node) && is(node, "A_Const") && node.A_Const.sval) return node.A_Const.sval.sval || null;
|
|
299
315
|
return null;
|
|
300
316
|
}
|
|
301
317
|
const JSONB_SET_RETURNING_FUNCTIONS = new Set([
|
|
@@ -312,21 +328,21 @@ function getFuncName(node) {
|
|
|
312
328
|
const names = node.FuncCall.funcname;
|
|
313
329
|
if (!names || names.length === 0) return null;
|
|
314
330
|
const last = names[names.length - 1];
|
|
315
|
-
if (isANode
|
|
331
|
+
if (isANode(last) && is(last, "String") && last.String.sval) return last.String.sval;
|
|
316
332
|
return null;
|
|
317
333
|
}
|
|
318
334
|
function getLastColumnRefField(columnRef) {
|
|
319
335
|
const fields = columnRef.ColumnRef.fields;
|
|
320
336
|
if (!fields || fields.length === 0) return null;
|
|
321
337
|
const lastField = fields[fields.length - 1];
|
|
322
|
-
if (isANode
|
|
338
|
+
if (isANode(lastField) && is(lastField, "String")) return lastField.String.sval || null;
|
|
323
339
|
return null;
|
|
324
340
|
}
|
|
325
341
|
function whereHasIsNotNull(whereClause, columnName) {
|
|
326
342
|
if (!whereClause) return false;
|
|
327
343
|
let found = false;
|
|
328
344
|
Walker.shallowMatch(whereClause, "NullTest", (node) => {
|
|
329
|
-
if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is
|
|
345
|
+
if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is(node.NullTest.arg, "ColumnRef")) {
|
|
330
346
|
if (getLastColumnRefField(node.NullTest.arg) === columnName) found = true;
|
|
331
347
|
}
|
|
332
348
|
});
|
|
@@ -347,41 +363,106 @@ const AGGREGATE_FUNCTIONS = new Set([
|
|
|
347
363
|
function containsAggregate(node) {
|
|
348
364
|
if (!node || typeof node !== "object") return false;
|
|
349
365
|
if (Array.isArray(node)) return node.some(containsAggregate);
|
|
350
|
-
if (isANode
|
|
366
|
+
if (isANode(node) && is(node, "FuncCall")) {
|
|
351
367
|
const funcname = node.FuncCall.funcname;
|
|
352
368
|
if (funcname) {
|
|
353
|
-
for (const f of funcname) if (isANode
|
|
369
|
+
for (const f of funcname) if (isANode(f) && is(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
|
|
354
370
|
}
|
|
355
371
|
}
|
|
356
|
-
if (isANode
|
|
372
|
+
if (isANode(node)) return containsAggregate(node[Object.keys(node)[0]]);
|
|
357
373
|
for (const child of Object.values(node)) if (containsAggregate(child)) return true;
|
|
358
374
|
return false;
|
|
359
375
|
}
|
|
360
376
|
function countBoolOrConditions(node) {
|
|
361
377
|
if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return 1;
|
|
362
378
|
let count = 0;
|
|
363
|
-
for (const arg of node.BoolExpr.args) if (isANode
|
|
379
|
+
for (const arg of node.BoolExpr.args) if (isANode(arg) && is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") count += countBoolOrConditions(arg);
|
|
364
380
|
else count += 1;
|
|
365
381
|
return count;
|
|
366
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Check whether every leaf of a top-level OR expression references the same
|
|
385
|
+
* left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).
|
|
386
|
+
* Returns false when ORs span different columns — IN rewrite doesn't apply.
|
|
387
|
+
*/
|
|
388
|
+
function allOrBranchesReferenceSameColumn(node) {
|
|
389
|
+
const columns = collectOrLeafColumns(node);
|
|
390
|
+
if (columns.length === 0) return false;
|
|
391
|
+
return columns.every((col) => col === columns[0]);
|
|
392
|
+
}
|
|
393
|
+
function collectOrLeafColumns(node) {
|
|
394
|
+
if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return [];
|
|
395
|
+
const columns = [];
|
|
396
|
+
for (const arg of node.BoolExpr.args) {
|
|
397
|
+
if (!isANode(arg)) continue;
|
|
398
|
+
if (is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") columns.push(...collectOrLeafColumns(arg));
|
|
399
|
+
else if (is(arg, "A_Expr")) {
|
|
400
|
+
const col = getLeftColumnKey(arg);
|
|
401
|
+
if (col) columns.push(col);
|
|
402
|
+
else return [];
|
|
403
|
+
} else return [];
|
|
404
|
+
}
|
|
405
|
+
return columns;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Get a string key for the left-hand column of an A_Expr equality comparison.
|
|
409
|
+
* For `t1.col = value` returns `"t1.col"`, for `col = value` returns `"col"`.
|
|
410
|
+
*/
|
|
411
|
+
function getLeftColumnKey(expr) {
|
|
412
|
+
if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;
|
|
413
|
+
if (!is(expr.A_Expr.lexpr, "ColumnRef")) return null;
|
|
414
|
+
const fields = expr.A_Expr.lexpr.ColumnRef.fields;
|
|
415
|
+
if (!fields) return null;
|
|
416
|
+
return fields.filter((f) => isANode(f) && is(f, "String")).map((f) => f.String.sval).join(".");
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Check if a WHERE clause contains an equality between columns from different
|
|
420
|
+
* tables (indicating an old-style implicit join condition).
|
|
421
|
+
*/
|
|
422
|
+
function whereHasCrossTableEquality(whereClause, tableNames) {
|
|
423
|
+
if (!whereClause) return false;
|
|
424
|
+
if (isANode(whereClause) && is(whereClause, "A_Expr")) {
|
|
425
|
+
if (whereClause.A_Expr.kind === "AEXPR_OP" && whereClause.A_Expr.name?.some((n) => is(n, "String") && n.String.sval === "=")) {
|
|
426
|
+
const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);
|
|
427
|
+
const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);
|
|
428
|
+
if (leftTable && rightTable && leftTable !== rightTable && tableNames.has(leftTable) && tableNames.has(rightTable)) return true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (isANode(whereClause) && is(whereClause, "BoolExpr")) {
|
|
432
|
+
for (const arg of whereClause.BoolExpr.args ?? []) if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) return true;
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Extract the table qualifier (first field) from a ColumnRef node.
|
|
438
|
+
* e.g. `t1.uid` → `"t1"`, `uid` → null
|
|
439
|
+
*/
|
|
440
|
+
function getColumnRefTableQualifier(node) {
|
|
441
|
+
if (!node || !isANode(node) || !is(node, "ColumnRef")) return null;
|
|
442
|
+
const fields = node.ColumnRef.fields;
|
|
443
|
+
if (!fields || fields.length < 2) return null;
|
|
444
|
+
const first = fields[0];
|
|
445
|
+
if (isANode(first) && is(first, "String")) return first.String.sval || null;
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
367
448
|
function isCountFuncCall(node) {
|
|
368
449
|
if (!node || typeof node !== "object") return false;
|
|
369
|
-
if (!isANode
|
|
450
|
+
if (!isANode(node) || !is(node, "FuncCall")) return false;
|
|
370
451
|
const fc = node.FuncCall;
|
|
371
|
-
if (!(fc.funcname?.some((n) => is
|
|
452
|
+
if (!(fc.funcname?.some((n) => is(n, "String") && n.String.sval === "count") ?? false)) return false;
|
|
372
453
|
if (fc.agg_star) return true;
|
|
373
|
-
if (fc.args && fc.args.length === 1 && isANode
|
|
454
|
+
if (fc.args && fc.args.length === 1 && isANode(fc.args[0]) && is(fc.args[0], "A_Const")) return true;
|
|
374
455
|
return false;
|
|
375
456
|
}
|
|
376
457
|
function isSubLinkWithCount(node) {
|
|
377
458
|
if (!node || typeof node !== "object") return false;
|
|
378
|
-
if (!isANode
|
|
459
|
+
if (!isANode(node) || !is(node, "SubLink")) return false;
|
|
379
460
|
const subselect = node.SubLink.subselect;
|
|
380
|
-
if (!subselect || !isANode
|
|
461
|
+
if (!subselect || !isANode(subselect) || !is(subselect, "SelectStmt")) return false;
|
|
381
462
|
const targets = subselect.SelectStmt.targetList;
|
|
382
463
|
if (!targets || targets.length !== 1) return false;
|
|
383
464
|
const target = targets[0];
|
|
384
|
-
if (!isANode
|
|
465
|
+
if (!isANode(target) || !is(target, "ResTarget") || !target.ResTarget.val) return false;
|
|
385
466
|
return isCountFuncCall(target.ResTarget.val);
|
|
386
467
|
}
|
|
387
468
|
function isCountExpression(node) {
|
|
@@ -389,7 +470,7 @@ function isCountExpression(node) {
|
|
|
389
470
|
}
|
|
390
471
|
function getIntegerConstantValue(node) {
|
|
391
472
|
if (!node || typeof node !== "object") return null;
|
|
392
|
-
if (!isANode
|
|
473
|
+
if (!isANode(node) || !is(node, "A_Const")) return null;
|
|
393
474
|
if (node.A_Const.ival === void 0) return null;
|
|
394
475
|
return node.A_Const.ival.ival ?? 0;
|
|
395
476
|
}
|
|
@@ -412,9 +493,8 @@ function isExistenceCheckPattern(lexpr, rexpr, op) {
|
|
|
412
493
|
}
|
|
413
494
|
return false;
|
|
414
495
|
}
|
|
415
|
-
|
|
416
496
|
//#endregion
|
|
417
|
-
//#region \0@oxc-project+runtime@0.
|
|
497
|
+
//#region \0@oxc-project+runtime@0.122.0/helpers/typeof.js
|
|
418
498
|
function _typeof(o) {
|
|
419
499
|
"@babel/helpers - typeof";
|
|
420
500
|
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
@@ -423,9 +503,8 @@ function _typeof(o) {
|
|
|
423
503
|
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
424
504
|
}, _typeof(o);
|
|
425
505
|
}
|
|
426
|
-
|
|
427
506
|
//#endregion
|
|
428
|
-
//#region \0@oxc-project+runtime@0.
|
|
507
|
+
//#region \0@oxc-project+runtime@0.122.0/helpers/toPrimitive.js
|
|
429
508
|
function toPrimitive(t, r) {
|
|
430
509
|
if ("object" != _typeof(t) || !t) return t;
|
|
431
510
|
var e = t[Symbol.toPrimitive];
|
|
@@ -436,16 +515,14 @@ function toPrimitive(t, r) {
|
|
|
436
515
|
}
|
|
437
516
|
return ("string" === r ? String : Number)(t);
|
|
438
517
|
}
|
|
439
|
-
|
|
440
518
|
//#endregion
|
|
441
|
-
//#region \0@oxc-project+runtime@0.
|
|
519
|
+
//#region \0@oxc-project+runtime@0.122.0/helpers/toPropertyKey.js
|
|
442
520
|
function toPropertyKey(t) {
|
|
443
521
|
var i = toPrimitive(t, "string");
|
|
444
522
|
return "symbol" == _typeof(i) ? i : i + "";
|
|
445
523
|
}
|
|
446
|
-
|
|
447
524
|
//#endregion
|
|
448
|
-
//#region \0@oxc-project+runtime@0.
|
|
525
|
+
//#region \0@oxc-project+runtime@0.122.0/helpers/defineProperty.js
|
|
449
526
|
function _defineProperty(e, r, t) {
|
|
450
527
|
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
451
528
|
value: t,
|
|
@@ -454,7 +531,6 @@ function _defineProperty(e, r, t) {
|
|
|
454
531
|
writable: !0
|
|
455
532
|
}) : e[r] = t, e;
|
|
456
533
|
}
|
|
457
|
-
|
|
458
534
|
//#endregion
|
|
459
535
|
//#region src/sql/walker.ts
|
|
460
536
|
const JSONB_EXTRACTION_OPS = new Set(["->", "->>"]);
|
|
@@ -701,17 +777,6 @@ var Walker = class Walker {
|
|
|
701
777
|
} else for (const [key, child] of Object.entries(node)) Walker.doTraverse(child, [...stack, key], callback);
|
|
702
778
|
}
|
|
703
779
|
};
|
|
704
|
-
function is(node, kind) {
|
|
705
|
-
return kind in node;
|
|
706
|
-
}
|
|
707
|
-
function getNodeKind(node) {
|
|
708
|
-
return Object.keys(node)[0];
|
|
709
|
-
}
|
|
710
|
-
function isANode(node) {
|
|
711
|
-
if (typeof node !== "object" || node === null) return false;
|
|
712
|
-
const keys = Object.keys(node);
|
|
713
|
-
return keys.length === 1 && /^[A-Z]/.test(keys[0]);
|
|
714
|
-
}
|
|
715
780
|
/**
|
|
716
781
|
* Given an operand of a comparison (e.g. the left side of `=`), check whether
|
|
717
782
|
* it is a JSONB path extraction expression such as `data->>'email'` or
|
|
@@ -763,7 +828,6 @@ function stripTableQualifiers(node) {
|
|
|
763
828
|
for (const value of Object.values(node)) if (Array.isArray(value)) for (const item of value) stripTableQualifiers(item);
|
|
764
829
|
else if (typeof value === "object" && value !== null) stripTableQualifiers(value);
|
|
765
830
|
}
|
|
766
|
-
|
|
767
831
|
//#endregion
|
|
768
832
|
//#region src/sql/analyzer.ts
|
|
769
833
|
const ignoredIdentifier = "__qd_placeholder";
|
|
@@ -989,10 +1053,17 @@ var Analyzer = class {
|
|
|
989
1053
|
};
|
|
990
1054
|
}
|
|
991
1055
|
};
|
|
992
|
-
|
|
993
1056
|
//#endregion
|
|
994
1057
|
//#region src/sql/database.ts
|
|
995
1058
|
const PostgresVersion = z.string().brand("PostgresVersion");
|
|
1059
|
+
/** Zod schema for PostgresExplainStage — validates common fields, passes through variant-specific ones. */
|
|
1060
|
+
const PostgresExplainStageSchema = z.lazy(() => z.object({
|
|
1061
|
+
"Node Type": z.string(),
|
|
1062
|
+
"Total Cost": z.number(),
|
|
1063
|
+
"Plan Width": z.number(),
|
|
1064
|
+
"Node Id": z.number().optional(),
|
|
1065
|
+
Plans: z.array(PostgresExplainStageSchema).optional()
|
|
1066
|
+
}).passthrough());
|
|
996
1067
|
/**
|
|
997
1068
|
* Drops a disabled index. Rollsback if it fails for any reason
|
|
998
1069
|
* @returns Did dropping the index succeed?
|
|
@@ -1009,7 +1080,6 @@ async function dropIndex(tx, index) {
|
|
|
1009
1080
|
return false;
|
|
1010
1081
|
}
|
|
1011
1082
|
}
|
|
1012
|
-
|
|
1013
1083
|
//#endregion
|
|
1014
1084
|
//#region src/sql/indexes.ts
|
|
1015
1085
|
function isIndexSupported(index) {
|
|
@@ -1022,7 +1092,6 @@ function isIndexSupported(index) {
|
|
|
1022
1092
|
function isIndexProbablyDroppable(index) {
|
|
1023
1093
|
return !index.is_primary && !index.is_unique;
|
|
1024
1094
|
}
|
|
1025
|
-
|
|
1026
1095
|
//#endregion
|
|
1027
1096
|
//#region src/sql/builder.ts
|
|
1028
1097
|
var PostgresQueryBuilder = class PostgresQueryBuilder {
|
|
@@ -1112,7 +1181,6 @@ var PostgresQueryBuilder = class PostgresQueryBuilder {
|
|
|
1112
1181
|
return query;
|
|
1113
1182
|
}
|
|
1114
1183
|
};
|
|
1115
|
-
|
|
1116
1184
|
//#endregion
|
|
1117
1185
|
//#region src/sql/pg-identifier.ts
|
|
1118
1186
|
/**
|
|
@@ -1324,7 +1392,6 @@ _defineProperty(PgIdentifier, "reservedKeywords", new Set([
|
|
|
1324
1392
|
"xmlserialize",
|
|
1325
1393
|
"xmltable"
|
|
1326
1394
|
]));
|
|
1327
|
-
|
|
1328
1395
|
//#endregion
|
|
1329
1396
|
//#region src/sql/permutations.ts
|
|
1330
1397
|
/**
|
|
@@ -1349,7 +1416,6 @@ function permutationsWithDescendingLength(arr) {
|
|
|
1349
1416
|
collected.sort((a, b) => b.length - a.length);
|
|
1350
1417
|
return collected;
|
|
1351
1418
|
}
|
|
1352
|
-
|
|
1353
1419
|
//#endregion
|
|
1354
1420
|
//#region src/optimizer/genalgo.ts
|
|
1355
1421
|
var IndexOptimizer = class IndexOptimizer {
|
|
@@ -1688,7 +1754,6 @@ var RollbackError = class {
|
|
|
1688
1754
|
};
|
|
1689
1755
|
const PROCEED = Symbol("PROCEED");
|
|
1690
1756
|
const SKIP = Symbol("SKIP");
|
|
1691
|
-
|
|
1692
1757
|
//#endregion
|
|
1693
1758
|
//#region src/optimizer/statistics.ts
|
|
1694
1759
|
const StatisticsSource = z.union([z.object({
|
|
@@ -2328,7 +2393,6 @@ _defineProperty(Statistics, "defaultStatsMode", Object.freeze({
|
|
|
2328
2393
|
reltuples: DEFAULT_RELTUPLES,
|
|
2329
2394
|
relpages: DEFAULT_RELPAGES
|
|
2330
2395
|
}));
|
|
2331
|
-
|
|
2332
2396
|
//#endregion
|
|
2333
2397
|
//#region src/optimizer/pss-rewriter.ts
|
|
2334
2398
|
/**
|
|
@@ -2355,7 +2419,7 @@ var PssRewriter = class {
|
|
|
2355
2419
|
});
|
|
2356
2420
|
}
|
|
2357
2421
|
};
|
|
2358
|
-
|
|
2359
2422
|
//#endregion
|
|
2360
|
-
export { Analyzer, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexOptimizer, PROCEED, PgIdentifier, PostgresQueryBuilder, PostgresVersion, PssRewriter, SKIP, Statistics, StatisticsMode, StatisticsSource, dropIndex, ignoredIdentifier, isIndexProbablyDroppable, isIndexSupported, parseNudges };
|
|
2423
|
+
export { Analyzer, ExportedStats, ExportedStatsColumns, ExportedStatsIndex, ExportedStatsStatistics, ExportedStatsV1, IndexOptimizer, PROCEED, PgIdentifier, PostgresExplainStageSchema, PostgresQueryBuilder, PostgresVersion, PssRewriter, SKIP, Statistics, StatisticsMode, StatisticsSource, dropIndex, ignoredIdentifier, isIndexProbablyDroppable, isIndexSupported, parseNudges };
|
|
2424
|
+
|
|
2361
2425
|
//# sourceMappingURL=index.mjs.map
|