@query-doctor/core 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.cjs +13 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs +13 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPrimitive.cjs +15 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPrimitive.mjs +15 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPropertyKey.cjs +10 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/toPropertyKey.mjs +10 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/typeof.cjs +17 -0
- package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/typeof.mjs +12 -0
- package/dist/_virtual/_rolldown/runtime.cjs +24 -0
- package/dist/index.cjs +33 -2568
- package/dist/index.d.cts +11 -781
- package/dist/index.d.mts +11 -781
- package/dist/index.mjs +10 -2522
- package/dist/optimizer/genalgo.cjs +365 -0
- package/dist/optimizer/genalgo.cjs.map +1 -0
- package/dist/optimizer/genalgo.d.cts +111 -0
- package/dist/optimizer/genalgo.d.cts.map +1 -0
- package/dist/optimizer/genalgo.d.mts +111 -0
- package/dist/optimizer/genalgo.d.mts.map +1 -0
- package/dist/optimizer/genalgo.mjs +362 -0
- package/dist/optimizer/genalgo.mjs.map +1 -0
- package/dist/optimizer/pss-rewriter.cjs +31 -0
- package/dist/optimizer/pss-rewriter.cjs.map +1 -0
- package/dist/optimizer/pss-rewriter.d.cts +16 -0
- package/dist/optimizer/pss-rewriter.d.cts.map +1 -0
- package/dist/optimizer/pss-rewriter.d.mts +16 -0
- package/dist/optimizer/pss-rewriter.d.mts.map +1 -0
- package/dist/optimizer/pss-rewriter.mjs +31 -0
- package/dist/optimizer/pss-rewriter.mjs.map +1 -0
- package/dist/optimizer/statistics.cjs +738 -0
- package/dist/optimizer/statistics.cjs.map +1 -0
- package/dist/optimizer/statistics.d.cts +389 -0
- package/dist/optimizer/statistics.d.cts.map +1 -0
- package/dist/optimizer/statistics.d.mts +389 -0
- package/dist/optimizer/statistics.d.mts.map +1 -0
- package/dist/optimizer/statistics.mjs +729 -0
- package/dist/optimizer/statistics.mjs.map +1 -0
- package/dist/sentry.cjs +13 -0
- package/dist/sentry.cjs.map +1 -0
- package/dist/sentry.d.cts +7 -0
- package/dist/sentry.d.cts.map +1 -0
- package/dist/sentry.d.mts +7 -0
- package/dist/sentry.d.mts.map +1 -0
- package/dist/sentry.mjs +13 -0
- package/dist/sentry.mjs.map +1 -0
- package/dist/sql/analyzer.cjs +242 -0
- package/dist/sql/analyzer.cjs.map +1 -0
- package/dist/sql/analyzer.d.cts +112 -0
- package/dist/sql/analyzer.d.cts.map +1 -0
- package/dist/sql/analyzer.d.mts +112 -0
- package/dist/sql/analyzer.d.mts.map +1 -0
- package/dist/sql/analyzer.mjs +240 -0
- package/dist/sql/analyzer.mjs.map +1 -0
- package/dist/sql/ast-utils.cjs +19 -0
- package/dist/sql/ast-utils.cjs.map +1 -0
- package/dist/sql/ast-utils.d.cts +9 -0
- package/dist/sql/ast-utils.d.cts.map +1 -0
- package/dist/sql/ast-utils.d.mts +9 -0
- package/dist/sql/ast-utils.d.mts.map +1 -0
- package/dist/sql/ast-utils.mjs +17 -0
- package/dist/sql/ast-utils.mjs.map +1 -0
- package/dist/sql/builder.cjs +94 -0
- package/dist/sql/builder.cjs.map +1 -0
- package/dist/sql/builder.d.cts +37 -0
- package/dist/sql/builder.d.cts.map +1 -0
- package/dist/sql/builder.d.mts +37 -0
- package/dist/sql/builder.d.mts.map +1 -0
- package/dist/sql/builder.mjs +94 -0
- package/dist/sql/builder.mjs.map +1 -0
- package/dist/sql/database.cjs +35 -0
- package/dist/sql/database.cjs.map +1 -0
- package/dist/sql/database.d.cts +91 -0
- package/dist/sql/database.d.cts.map +1 -0
- package/dist/sql/database.d.mts +91 -0
- package/dist/sql/database.d.mts.map +1 -0
- package/dist/sql/database.mjs +32 -0
- package/dist/sql/database.mjs.map +1 -0
- package/dist/sql/indexes.cjs +17 -0
- package/dist/sql/indexes.cjs.map +1 -0
- package/dist/sql/indexes.d.cts +14 -0
- package/dist/sql/indexes.d.cts.map +1 -0
- package/dist/sql/indexes.d.mts +14 -0
- package/dist/sql/indexes.d.mts.map +1 -0
- package/dist/sql/indexes.mjs +16 -0
- package/dist/sql/indexes.mjs.map +1 -0
- package/dist/sql/nudges.cjs +484 -0
- package/dist/sql/nudges.cjs.map +1 -0
- package/dist/sql/nudges.d.cts +21 -0
- package/dist/sql/nudges.d.cts.map +1 -0
- package/dist/sql/nudges.d.mts +21 -0
- package/dist/sql/nudges.d.mts.map +1 -0
- package/dist/sql/nudges.mjs +484 -0
- package/dist/sql/nudges.mjs.map +1 -0
- package/dist/sql/permutations.cjs +28 -0
- package/dist/sql/permutations.cjs.map +1 -0
- package/dist/sql/permutations.mjs +28 -0
- package/dist/sql/permutations.mjs.map +1 -0
- package/dist/sql/pg-identifier.cjs +216 -0
- package/dist/sql/pg-identifier.cjs.map +1 -0
- package/dist/sql/pg-identifier.d.cts +31 -0
- package/dist/sql/pg-identifier.d.cts.map +1 -0
- package/dist/sql/pg-identifier.d.mts +31 -0
- package/dist/sql/pg-identifier.d.mts.map +1 -0
- package/dist/sql/pg-identifier.mjs +216 -0
- package/dist/sql/pg-identifier.mjs.map +1 -0
- package/dist/sql/walker.cjs +314 -0
- package/dist/sql/walker.cjs.map +1 -0
- package/dist/sql/walker.d.cts +15 -0
- package/dist/sql/walker.d.cts.map +1 -0
- package/dist/sql/walker.d.mts +15 -0
- package/dist/sql/walker.d.mts.map +1 -0
- package/dist/sql/walker.mjs +313 -0
- package/dist/sql/walker.mjs.map +1 -0
- package/package.json +2 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { is, isANode } from "./ast-utils.mjs";
|
|
3
|
+
import { Walker } from "./walker.mjs";
|
|
4
|
+
//#region src/sql/nudges.ts
|
|
5
|
+
const findFuncCallsOnColumns = (whereClause) => {
|
|
6
|
+
const nudges = [];
|
|
7
|
+
Walker.shallowMatch(whereClause, "FuncCall", (node) => {
|
|
8
|
+
if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) nudges.push({
|
|
9
|
+
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
10
|
+
severity: "WARNING",
|
|
11
|
+
message: "Avoid using functions on columns in conditions — prevents index usage",
|
|
12
|
+
location: node.FuncCall.location
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
Walker.shallowMatch(whereClause, "CoalesceExpr", (node) => {
|
|
16
|
+
if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) nudges.push({
|
|
17
|
+
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
18
|
+
severity: "WARNING",
|
|
19
|
+
message: "Avoid using functions on columns in conditions — prevents index usage",
|
|
20
|
+
location: node.CoalesceExpr.location
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
Walker.shallowMatch(whereClause, "TypeCast", (node) => {
|
|
24
|
+
if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) nudges.push({
|
|
25
|
+
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
26
|
+
severity: "WARNING",
|
|
27
|
+
message: "Avoid using functions on columns in WHERE clause",
|
|
28
|
+
location: node.TypeCast.location
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
return nudges;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Detect nudges for a single node during AST traversal.
|
|
35
|
+
* Returns an array of nudges found for this node.
|
|
36
|
+
*/
|
|
37
|
+
function parseNudges(node, stack) {
|
|
38
|
+
const nudges = [];
|
|
39
|
+
if (is(node, "SelectStmt")) {
|
|
40
|
+
if (node.SelectStmt.whereClause) nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));
|
|
41
|
+
const star = node.SelectStmt.targetList?.find((target) => {
|
|
42
|
+
if (!(is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "ColumnRef"))) return false;
|
|
43
|
+
const fields = target.ResTarget.val.ColumnRef.fields;
|
|
44
|
+
if (!fields?.some((field) => is(field, "A_Star"))) return false;
|
|
45
|
+
if (fields.length > 1) return false;
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
if (star) {
|
|
49
|
+
const fromClause = node.SelectStmt.fromClause;
|
|
50
|
+
if (!(fromClause && fromClause.length > 0 && fromClause.every((item) => is(item, "RangeSubselect")))) nudges.push({
|
|
51
|
+
kind: "AVOID_SELECT_STAR",
|
|
52
|
+
severity: "INFO",
|
|
53
|
+
message: "Avoid using SELECT *",
|
|
54
|
+
location: star.ResTarget.location
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
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({
|
|
58
|
+
kind: "AVOID_SCALAR_SUBQUERY_IN_SELECT",
|
|
59
|
+
severity: "WARNING",
|
|
60
|
+
message: "Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN",
|
|
61
|
+
location: target.ResTarget.val.SubLink.location
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (is(node, "SelectStmt")) {
|
|
65
|
+
if (!stack.some((item) => item === "RangeSubselect" || item === "SubLink" || item === "CommonTableExpr")) {
|
|
66
|
+
if (node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0) {
|
|
67
|
+
if (node.SelectStmt.fromClause.some((fromItem) => {
|
|
68
|
+
return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
|
|
69
|
+
})) {
|
|
70
|
+
const fromLocation = node.SelectStmt.fromClause.find((item) => is(item, "RangeVar"))?.RangeVar.location;
|
|
71
|
+
if (!node.SelectStmt.whereClause) nudges.push({
|
|
72
|
+
kind: "MISSING_WHERE_CLAUSE",
|
|
73
|
+
severity: "INFO",
|
|
74
|
+
message: "Missing WHERE clause",
|
|
75
|
+
location: fromLocation
|
|
76
|
+
});
|
|
77
|
+
if (!node.SelectStmt.limitCount) nudges.push({
|
|
78
|
+
kind: "MISSING_LIMIT_CLAUSE",
|
|
79
|
+
severity: "INFO",
|
|
80
|
+
message: "Missing LIMIT clause",
|
|
81
|
+
location: fromLocation
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (is(node, "SelectStmt") && node.SelectStmt.sortClause) for (const sortItem of node.SelectStmt.sortClause) {
|
|
88
|
+
if (!is(sortItem, "SortBy")) continue;
|
|
89
|
+
const sortDir = sortItem.SortBy.sortby_dir ?? "SORTBY_DEFAULT";
|
|
90
|
+
const sortNulls = sortItem.SortBy.sortby_nulls ?? "SORTBY_NULLS_DEFAULT";
|
|
91
|
+
if (sortDir === "SORTBY_DESC" && sortNulls === "SORTBY_NULLS_DEFAULT") {
|
|
92
|
+
if (sortItem.SortBy.node && is(sortItem.SortBy.node, "ColumnRef")) {
|
|
93
|
+
const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);
|
|
94
|
+
if (!(sortColumnName !== null && whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName))) nudges.push({
|
|
95
|
+
kind: "NULLS_FIRST_IN_DESC_ORDER",
|
|
96
|
+
severity: "INFO",
|
|
97
|
+
message: "ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end",
|
|
98
|
+
location: sortItem.SortBy.node.ColumnRef.location
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (is(node, "A_Expr")) {
|
|
104
|
+
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 === "<>")) {
|
|
105
|
+
const leftIsNull = isNullConstant(node.A_Expr.lexpr);
|
|
106
|
+
const rightIsNull = isNullConstant(node.A_Expr.rexpr);
|
|
107
|
+
if (leftIsNull || rightIsNull) nudges.push({
|
|
108
|
+
kind: "USE_IS_NULL_NOT_EQUALS",
|
|
109
|
+
severity: "WARNING",
|
|
110
|
+
message: "Use IS NULL instead of = or != or <> for NULL comparisons",
|
|
111
|
+
location: node.A_Expr.location
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if ((node.A_Expr.kind === "AEXPR_LIKE" || node.A_Expr.kind === "AEXPR_ILIKE") && node.A_Expr.rexpr) {
|
|
115
|
+
const patternString = getStringConstantValue(node.A_Expr.rexpr);
|
|
116
|
+
if (patternString && patternString.startsWith("%")) {
|
|
117
|
+
let stringNode;
|
|
118
|
+
if (is(node.A_Expr.rexpr, "A_Const")) stringNode = node.A_Expr.rexpr.A_Const;
|
|
119
|
+
nudges.push({
|
|
120
|
+
kind: "AVOID_LEADING_WILDCARD_LIKE",
|
|
121
|
+
severity: "WARNING",
|
|
122
|
+
message: "Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search",
|
|
123
|
+
location: stringNode?.location
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (is(node, "SelectStmt") && node.SelectStmt.sortClause) {
|
|
129
|
+
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({
|
|
130
|
+
kind: "AVOID_ORDER_BY_RANDOM",
|
|
131
|
+
severity: "WARNING",
|
|
132
|
+
message: "Avoid using ORDER BY random()",
|
|
133
|
+
location: sortItem.SortBy.node.FuncCall.location
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (is(node, "SelectStmt") && node.SelectStmt.distinctClause) nudges.push({
|
|
137
|
+
kind: "AVOID_DISTINCT_WITHOUT_REASON",
|
|
138
|
+
severity: "WARNING",
|
|
139
|
+
message: "Avoid using DISTINCT without a reason"
|
|
140
|
+
});
|
|
141
|
+
if (is(node, "JoinExpr")) {
|
|
142
|
+
if (node.JoinExpr.quals) nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));
|
|
143
|
+
else if (!node.JoinExpr.usingClause) nudges.push({
|
|
144
|
+
kind: "MISSING_JOIN_CONDITION",
|
|
145
|
+
severity: "WARNING",
|
|
146
|
+
message: "Missing JOIN condition"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
|
|
150
|
+
const tables = node.SelectStmt.fromClause.filter((item) => is(item, "RangeVar"));
|
|
151
|
+
if (tables.length > 1) {
|
|
152
|
+
const tableNames = new Set(tables.map((t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || ""));
|
|
153
|
+
if (!whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)) nudges.push({
|
|
154
|
+
kind: "MISSING_JOIN_CONDITION",
|
|
155
|
+
severity: "WARNING",
|
|
156
|
+
message: "Missing JOIN condition",
|
|
157
|
+
location: tables[1].RangeVar.location
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (is(node, "BoolExpr") && node.BoolExpr.boolop === "OR_EXPR") {
|
|
162
|
+
if (countBoolOrConditions(node) >= 3 && allOrBranchesReferenceSameColumn(node)) nudges.push({
|
|
163
|
+
kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
|
|
164
|
+
severity: "WARNING",
|
|
165
|
+
message: "Consider using IN instead of many ORs",
|
|
166
|
+
location: node.BoolExpr.location
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (is(node, "BoolExpr") && node.BoolExpr.boolop === "NOT_EXPR") {
|
|
170
|
+
const args = node.BoolExpr.args;
|
|
171
|
+
if (args && args.length === 1) {
|
|
172
|
+
const inner = args[0];
|
|
173
|
+
if (isANode(inner) && is(inner, "SubLink") && inner.SubLink.subLinkType === "ANY_SUBLINK") nudges.push({
|
|
174
|
+
kind: "PREFER_NOT_EXISTS_OVER_NOT_IN",
|
|
175
|
+
severity: "WARNING",
|
|
176
|
+
message: "Prefer NOT EXISTS over NOT IN (SELECT ...)",
|
|
177
|
+
location: inner.SubLink.location
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (is(node, "FuncCall")) {
|
|
182
|
+
const funcName = node.FuncCall.funcname;
|
|
183
|
+
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({
|
|
184
|
+
kind: "PREFER_COUNT_STAR_OVER_COUNT_COLUMN",
|
|
185
|
+
severity: "INFO",
|
|
186
|
+
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.",
|
|
187
|
+
location: node.FuncCall.location
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (is(node, "SelectStmt") && node.SelectStmt.havingClause) {
|
|
191
|
+
if (!containsAggregate(node.SelectStmt.havingClause)) {
|
|
192
|
+
const having = node.SelectStmt.havingClause;
|
|
193
|
+
let location;
|
|
194
|
+
if (is(having, "A_Expr")) location = having.A_Expr.location;
|
|
195
|
+
else if (is(having, "BoolExpr")) location = having.BoolExpr.location;
|
|
196
|
+
nudges.push({
|
|
197
|
+
kind: "PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES",
|
|
198
|
+
severity: "INFO",
|
|
199
|
+
message: "Non-aggregate condition in HAVING should be in WHERE",
|
|
200
|
+
location
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (is(node, "FuncCall")) {
|
|
205
|
+
if (stack.some((item) => item === "whereClause") && node.FuncCall.args) {
|
|
206
|
+
const name = getFuncName(node);
|
|
207
|
+
if (name && JSONB_SET_RETURNING_FUNCTIONS.has(name) && containsColumnRef(node.FuncCall.args)) nudges.push({
|
|
208
|
+
kind: "CONSIDER_JSONB_CONTAINMENT_OPERATOR",
|
|
209
|
+
severity: "INFO",
|
|
210
|
+
message: "JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.",
|
|
211
|
+
location: node.FuncCall.location
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (is(node, "A_Expr")) {
|
|
216
|
+
if (node.A_Expr.kind === "AEXPR_IN") {
|
|
217
|
+
let list;
|
|
218
|
+
if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, "List")) list = node.A_Expr.lexpr.List;
|
|
219
|
+
else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, "List")) list = node.A_Expr.rexpr.List;
|
|
220
|
+
if (list?.items && list.items.length >= 10) nudges.push({
|
|
221
|
+
kind: "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY",
|
|
222
|
+
message: "`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter",
|
|
223
|
+
severity: "INFO",
|
|
224
|
+
location: node.A_Expr.location
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (is(node, "FuncCall")) {
|
|
229
|
+
const funcname = node.FuncCall.funcname?.[0] && is(node.FuncCall.funcname[0], "String") && node.FuncCall.funcname[0].String.sval;
|
|
230
|
+
if (funcname && [
|
|
231
|
+
"sum",
|
|
232
|
+
"count",
|
|
233
|
+
"avg",
|
|
234
|
+
"min",
|
|
235
|
+
"max"
|
|
236
|
+
].includes(funcname.toLowerCase())) {
|
|
237
|
+
const firstArg = node.FuncCall.args?.[0];
|
|
238
|
+
if (firstArg && isANode(firstArg) && is(firstArg, "CaseExpr")) {
|
|
239
|
+
const caseExpr = firstArg.CaseExpr;
|
|
240
|
+
if (caseExpr.args && caseExpr.args.length === 1) {
|
|
241
|
+
const defresult = caseExpr.defresult;
|
|
242
|
+
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({
|
|
243
|
+
kind: "PREFER_FILTER_OVER_CASE_IN_AGGREGATE",
|
|
244
|
+
severity: "INFO",
|
|
245
|
+
message: "Use FILTER (WHERE ...) instead of CASE inside aggregate functions",
|
|
246
|
+
location: node.FuncCall.location
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (is(node, "SelectStmt") && node.SelectStmt.op === "SETOP_UNION" && !node.SelectStmt.all) nudges.push({
|
|
253
|
+
kind: "PREFER_UNION_ALL_OVER_UNION",
|
|
254
|
+
severity: "INFO",
|
|
255
|
+
message: "UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed"
|
|
256
|
+
});
|
|
257
|
+
if (is(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP" && node.A_Expr.name && node.A_Expr.name.length > 0) {
|
|
258
|
+
const opNode = node.A_Expr.name[0];
|
|
259
|
+
const op = is(opNode, "String") ? opNode.String.sval : null;
|
|
260
|
+
if (op && isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)) nudges.push({
|
|
261
|
+
kind: "USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK",
|
|
262
|
+
severity: "INFO",
|
|
263
|
+
message: "Use EXISTS instead of COUNT for existence checks",
|
|
264
|
+
location: node.A_Expr.location
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return nudges;
|
|
268
|
+
}
|
|
269
|
+
function containsColumnRef(args) {
|
|
270
|
+
for (const arg of args) if (hasColumnRefInNode(arg)) return true;
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
function hasColumnRefInNode(node) {
|
|
274
|
+
if (isANode(node) && is(node, "ColumnRef")) return true;
|
|
275
|
+
if (typeof node !== "object" || node === null) return false;
|
|
276
|
+
if (Array.isArray(node)) return node.some((item) => hasColumnRefInNode(item));
|
|
277
|
+
if (isANode(node)) return hasColumnRefInNode(node[Object.keys(node)[0]]);
|
|
278
|
+
for (const child of Object.values(node)) if (hasColumnRefInNode(child)) return true;
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
function hasActualTablesInJoin(joinExpr) {
|
|
282
|
+
if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "RangeVar")) return true;
|
|
283
|
+
if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, "JoinExpr")) {
|
|
284
|
+
if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) return true;
|
|
285
|
+
}
|
|
286
|
+
if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "RangeVar")) return true;
|
|
287
|
+
if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, "JoinExpr")) {
|
|
288
|
+
if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
function isNullConstant(node) {
|
|
293
|
+
if (!node || typeof node !== "object") return false;
|
|
294
|
+
if (isANode(node) && is(node, "A_Const")) return node.A_Const.isnull !== void 0;
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
function getStringConstantValue(node) {
|
|
298
|
+
if (!node || typeof node !== "object") return null;
|
|
299
|
+
if (isANode(node) && is(node, "A_Const") && node.A_Const.sval) return node.A_Const.sval.sval || null;
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const JSONB_SET_RETURNING_FUNCTIONS = new Set([
|
|
303
|
+
"jsonb_array_elements",
|
|
304
|
+
"json_array_elements",
|
|
305
|
+
"jsonb_array_elements_text",
|
|
306
|
+
"json_array_elements_text",
|
|
307
|
+
"jsonb_each",
|
|
308
|
+
"json_each",
|
|
309
|
+
"jsonb_each_text",
|
|
310
|
+
"json_each_text"
|
|
311
|
+
]);
|
|
312
|
+
function getFuncName(node) {
|
|
313
|
+
const names = node.FuncCall.funcname;
|
|
314
|
+
if (!names || names.length === 0) return null;
|
|
315
|
+
const last = names[names.length - 1];
|
|
316
|
+
if (isANode(last) && is(last, "String") && last.String.sval) return last.String.sval;
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
function getLastColumnRefField(columnRef) {
|
|
320
|
+
const fields = columnRef.ColumnRef.fields;
|
|
321
|
+
if (!fields || fields.length === 0) return null;
|
|
322
|
+
const lastField = fields[fields.length - 1];
|
|
323
|
+
if (isANode(lastField) && is(lastField, "String")) return lastField.String.sval || null;
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
function whereHasIsNotNull(whereClause, columnName) {
|
|
327
|
+
if (!whereClause) return false;
|
|
328
|
+
let found = false;
|
|
329
|
+
Walker.shallowMatch(whereClause, "NullTest", (node) => {
|
|
330
|
+
if (node.NullTest.nulltesttype === "IS_NOT_NULL" && node.NullTest.arg && is(node.NullTest.arg, "ColumnRef")) {
|
|
331
|
+
if (getLastColumnRefField(node.NullTest.arg) === columnName) found = true;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
return found;
|
|
335
|
+
}
|
|
336
|
+
const AGGREGATE_FUNCTIONS = new Set([
|
|
337
|
+
"count",
|
|
338
|
+
"sum",
|
|
339
|
+
"avg",
|
|
340
|
+
"min",
|
|
341
|
+
"max",
|
|
342
|
+
"array_agg",
|
|
343
|
+
"string_agg",
|
|
344
|
+
"bool_and",
|
|
345
|
+
"bool_or",
|
|
346
|
+
"every"
|
|
347
|
+
]);
|
|
348
|
+
function containsAggregate(node) {
|
|
349
|
+
if (!node || typeof node !== "object") return false;
|
|
350
|
+
if (Array.isArray(node)) return node.some(containsAggregate);
|
|
351
|
+
if (isANode(node) && is(node, "FuncCall")) {
|
|
352
|
+
const funcname = node.FuncCall.funcname;
|
|
353
|
+
if (funcname) {
|
|
354
|
+
for (const f of funcname) if (isANode(f) && is(f, "String") && AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? "")) return true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (isANode(node)) return containsAggregate(node[Object.keys(node)[0]]);
|
|
358
|
+
for (const child of Object.values(node)) if (containsAggregate(child)) return true;
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
function countBoolOrConditions(node) {
|
|
362
|
+
if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return 1;
|
|
363
|
+
let count = 0;
|
|
364
|
+
for (const arg of node.BoolExpr.args) if (isANode(arg) && is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") count += countBoolOrConditions(arg);
|
|
365
|
+
else count += 1;
|
|
366
|
+
return count;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Check whether every leaf of a top-level OR expression references the same
|
|
370
|
+
* left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).
|
|
371
|
+
* Returns false when ORs span different columns — IN rewrite doesn't apply.
|
|
372
|
+
*/
|
|
373
|
+
function allOrBranchesReferenceSameColumn(node) {
|
|
374
|
+
const columns = collectOrLeafColumns(node);
|
|
375
|
+
if (columns.length === 0) return false;
|
|
376
|
+
return columns.every((col) => col === columns[0]);
|
|
377
|
+
}
|
|
378
|
+
function collectOrLeafColumns(node) {
|
|
379
|
+
if (node.BoolExpr.boolop !== "OR_EXPR" || !node.BoolExpr.args) return [];
|
|
380
|
+
const columns = [];
|
|
381
|
+
for (const arg of node.BoolExpr.args) {
|
|
382
|
+
if (!isANode(arg)) continue;
|
|
383
|
+
if (is(arg, "BoolExpr") && arg.BoolExpr.boolop === "OR_EXPR") columns.push(...collectOrLeafColumns(arg));
|
|
384
|
+
else if (is(arg, "A_Expr")) {
|
|
385
|
+
const col = getLeftColumnKey(arg);
|
|
386
|
+
if (col) columns.push(col);
|
|
387
|
+
else return [];
|
|
388
|
+
} else return [];
|
|
389
|
+
}
|
|
390
|
+
return columns;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get a string key for the left-hand column of an A_Expr equality comparison.
|
|
394
|
+
* For `t1.col = value` returns `"t1.col"`, for `col = value` returns `"col"`.
|
|
395
|
+
*/
|
|
396
|
+
function getLeftColumnKey(expr) {
|
|
397
|
+
if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;
|
|
398
|
+
if (!is(expr.A_Expr.lexpr, "ColumnRef")) return null;
|
|
399
|
+
const fields = expr.A_Expr.lexpr.ColumnRef.fields;
|
|
400
|
+
if (!fields) return null;
|
|
401
|
+
return fields.filter((f) => isANode(f) && is(f, "String")).map((f) => f.String.sval).join(".");
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Check if a WHERE clause contains an equality between columns from different
|
|
405
|
+
* tables (indicating an old-style implicit join condition).
|
|
406
|
+
*/
|
|
407
|
+
function whereHasCrossTableEquality(whereClause, tableNames) {
|
|
408
|
+
if (!whereClause) return false;
|
|
409
|
+
if (isANode(whereClause) && is(whereClause, "A_Expr")) {
|
|
410
|
+
if (whereClause.A_Expr.kind === "AEXPR_OP" && whereClause.A_Expr.name?.some((n) => is(n, "String") && n.String.sval === "=")) {
|
|
411
|
+
const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);
|
|
412
|
+
const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);
|
|
413
|
+
if (leftTable && rightTable && leftTable !== rightTable && tableNames.has(leftTable) && tableNames.has(rightTable)) return true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (isANode(whereClause) && is(whereClause, "BoolExpr")) {
|
|
417
|
+
for (const arg of whereClause.BoolExpr.args ?? []) if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) return true;
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Extract the table qualifier (first field) from a ColumnRef node.
|
|
423
|
+
* e.g. `t1.uid` → `"t1"`, `uid` → null
|
|
424
|
+
*/
|
|
425
|
+
function getColumnRefTableQualifier(node) {
|
|
426
|
+
if (!node || !isANode(node) || !is(node, "ColumnRef")) return null;
|
|
427
|
+
const fields = node.ColumnRef.fields;
|
|
428
|
+
if (!fields || fields.length < 2) return null;
|
|
429
|
+
const first = fields[0];
|
|
430
|
+
if (isANode(first) && is(first, "String")) return first.String.sval || null;
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
function isCountFuncCall(node) {
|
|
434
|
+
if (!node || typeof node !== "object") return false;
|
|
435
|
+
if (!isANode(node) || !is(node, "FuncCall")) return false;
|
|
436
|
+
const fc = node.FuncCall;
|
|
437
|
+
if (!(fc.funcname?.some((n) => is(n, "String") && n.String.sval === "count") ?? false)) return false;
|
|
438
|
+
if (fc.agg_star) return true;
|
|
439
|
+
if (fc.args && fc.args.length === 1 && isANode(fc.args[0]) && is(fc.args[0], "A_Const")) return true;
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
function isSubLinkWithCount(node) {
|
|
443
|
+
if (!node || typeof node !== "object") return false;
|
|
444
|
+
if (!isANode(node) || !is(node, "SubLink")) return false;
|
|
445
|
+
const subselect = node.SubLink.subselect;
|
|
446
|
+
if (!subselect || !isANode(subselect) || !is(subselect, "SelectStmt")) return false;
|
|
447
|
+
const targets = subselect.SelectStmt.targetList;
|
|
448
|
+
if (!targets || targets.length !== 1) return false;
|
|
449
|
+
const target = targets[0];
|
|
450
|
+
if (!isANode(target) || !is(target, "ResTarget") || !target.ResTarget.val) return false;
|
|
451
|
+
return isCountFuncCall(target.ResTarget.val);
|
|
452
|
+
}
|
|
453
|
+
function isCountExpression(node) {
|
|
454
|
+
return isCountFuncCall(node) || isSubLinkWithCount(node);
|
|
455
|
+
}
|
|
456
|
+
function getIntegerConstantValue(node) {
|
|
457
|
+
if (!node || typeof node !== "object") return null;
|
|
458
|
+
if (!isANode(node) || !is(node, "A_Const")) return null;
|
|
459
|
+
if (node.A_Const.ival === void 0) return null;
|
|
460
|
+
return node.A_Const.ival.ival ?? 0;
|
|
461
|
+
}
|
|
462
|
+
function isExistenceCheckPattern(lexpr, rexpr, op) {
|
|
463
|
+
if (isCountExpression(lexpr)) {
|
|
464
|
+
const val = getIntegerConstantValue(rexpr);
|
|
465
|
+
if (val !== null) {
|
|
466
|
+
if (op === ">" && val === 0) return true;
|
|
467
|
+
if (op === ">=" && val === 1) return true;
|
|
468
|
+
if ((op === "!=" || op === "<>") && val === 0) return true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (isCountExpression(rexpr)) {
|
|
472
|
+
const val = getIntegerConstantValue(lexpr);
|
|
473
|
+
if (val !== null) {
|
|
474
|
+
if (op === "<" && val === 0) return true;
|
|
475
|
+
if (op === "<=" && val === 1) return true;
|
|
476
|
+
if ((op === "!=" || op === "<>") && val === 0) return true;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
export { parseNudges };
|
|
483
|
+
|
|
484
|
+
//# sourceMappingURL=nudges.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nudges.mjs","names":[],"sources":["../../src/sql/nudges.ts"],"sourcesContent":["import type { A_Const, List, Node, ResTarget } from \"@pgsql/types\";\nimport { is, isANode, type KeysOfUnion } from \"./ast-utils.js\";\nimport { Walker } from \"./walker.js\";\n\ntype NudgeKind =\n | \"LARGE_IMPROVEMENT_FOUND\"\n | \"SMALL_IMPROVEMENT_FOUND\"\n | \"AVOID_SELECT_STAR\"\n | \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\"\n | \"MISSING_WHERE_CLAUSE\"\n | \"MISSING_LIMIT_CLAUSE\"\n | \"USE_IS_NULL_NOT_EQUALS\"\n | \"AVOID_DISTINCT_WITHOUT_REASON\"\n | \"MISSING_JOIN_CONDITION\"\n | \"AVOID_LEADING_WILDCARD_LIKE\"\n | \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\"\n | \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\"\n | \"PREFER_NOT_EXISTS_OVER_NOT_IN\"\n | \"AVOID_ORDER_BY_RANDOM\"\n | \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\"\n | \"PREFER_UNION_ALL_OVER_UNION\"\n | \"NULLS_FIRST_IN_DESC_ORDER\"\n | \"AVOID_SCALAR_SUBQUERY_IN_SELECT\"\n | \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\"\n | \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\"\n | \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\"\n | \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\";\n\nexport type Nudge = {\n kind: NudgeKind;\n severity: \"CRITICAL\" | \"WARNING\" | \"INFO\";\n message: string;\n location?: number;\n};\n\ntype NudgeCreator = (node: Node) => Nudge[];\n\nconst findFuncCallsOnColumns: NudgeCreator = (whereClause) => {\n const nudges: Nudge[] = [];\n Walker.shallowMatch(whereClause, \"FuncCall\", (node) => {\n if (node.FuncCall.args && containsColumnRef(node.FuncCall.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.FuncCall.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"CoalesceExpr\", (node) => {\n if (node.CoalesceExpr.args && containsColumnRef(node.CoalesceExpr.args)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message:\n \"Avoid using functions on columns in conditions — prevents index usage\",\n location: node.CoalesceExpr.location,\n });\n }\n });\n Walker.shallowMatch(whereClause, \"TypeCast\", (node) => {\n if (node.TypeCast.arg && hasColumnRefInNode(node.TypeCast.arg)) {\n nudges.push({\n kind: \"AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE\",\n severity: \"WARNING\",\n message: \"Avoid using functions on columns in WHERE clause\",\n location: node.TypeCast.location,\n });\n }\n });\n return nudges;\n};\n\n/**\n * Detect nudges for a single node during AST traversal.\n * Returns an array of nudges found for this node.\n */\nexport function parseNudges(\n node: Node,\n stack: (KeysOfUnion<Node> | string)[],\n): Nudge[] {\n const nudges: Nudge[] = [];\n\n if (is(node, \"SelectStmt\")) {\n if (node.SelectStmt.whereClause) {\n nudges.push(...findFuncCallsOnColumns(node.SelectStmt.whereClause));\n }\n const star = node.SelectStmt.targetList?.find(\n (target): target is { ResTarget: ResTarget } => {\n if (\n !(\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"ColumnRef\")\n )\n ) {\n return false;\n }\n const fields = target.ResTarget.val.ColumnRef.fields;\n if (!fields?.some((field) => is(field, \"A_Star\"))) {\n return false;\n }\n // Qualified star (e.g. users.*) is deliberate — skip\n if (fields.length > 1) {\n return false;\n }\n return true;\n },\n );\n if (star) {\n // Skip when all FROM items are subqueries — the outer * just\n // passes through whatever the subquery already selected.\n const fromClause = node.SelectStmt.fromClause;\n const allSubselects =\n fromClause &&\n fromClause.length > 0 &&\n fromClause.every((item) => is(item, \"RangeSubselect\"));\n\n if (!allSubselects) {\n nudges.push({\n kind: \"AVOID_SELECT_STAR\",\n severity: \"INFO\",\n message: \"Avoid using SELECT *\",\n location: star.ResTarget.location,\n });\n }\n }\n\n // Detect correlated scalar subqueries in SELECT (N+1 problem)\n for (const target of node.SelectStmt.targetList ?? []) {\n if (\n is(target, \"ResTarget\") &&\n target.ResTarget.val &&\n is(target.ResTarget.val, \"SubLink\") &&\n target.ResTarget.val.SubLink.subLinkType === \"EXPR_SUBLINK\"\n ) {\n nudges.push({\n kind: \"AVOID_SCALAR_SUBQUERY_IN_SELECT\",\n severity: \"WARNING\",\n message:\n \"Avoid correlated scalar subqueries in SELECT; consider rewriting as a JOIN\",\n location: target.ResTarget.val.SubLink.location,\n });\n }\n }\n }\n\n // Detect unbounded queries (missing WHERE or LIMIT on table queries)\n if (is(node, \"SelectStmt\")) {\n // Only check top-level SELECT statements (not subqueries)\n const isSubquery = stack.some(\n (item) =>\n item === \"RangeSubselect\" ||\n item === \"SubLink\" ||\n item === \"CommonTableExpr\",\n );\n\n if (!isSubquery) {\n const hasFromClause =\n node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 0;\n if (hasFromClause) {\n // Check if this SELECT queries actual tables (not just subqueries or CTEs)\n const hasActualTables = node.SelectStmt.fromClause!.some((fromItem) => {\n return (\n is(fromItem, \"RangeVar\") ||\n (is(fromItem, \"JoinExpr\") && hasActualTablesInJoin(fromItem))\n );\n });\n\n if (hasActualTables) {\n const firstTable = node.SelectStmt.fromClause!.find((item) =>\n is(item, \"RangeVar\"),\n );\n const fromLocation = firstTable?.RangeVar.location;\n\n if (!node.SelectStmt.whereClause) {\n nudges.push({\n kind: \"MISSING_WHERE_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing WHERE clause\",\n location: fromLocation,\n });\n }\n\n if (!node.SelectStmt.limitCount) {\n nudges.push({\n kind: \"MISSING_LIMIT_CLAUSE\",\n severity: \"INFO\",\n message: \"Missing LIMIT clause\",\n location: fromLocation,\n });\n }\n }\n }\n }\n }\n\n // Detect ORDER BY DESC without explicit NULLS LAST (NULLs sort first by default)\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (!is(sortItem, \"SortBy\")) continue;\n\n const sortDir = sortItem.SortBy.sortby_dir ?? \"SORTBY_DEFAULT\";\n const sortNulls = sortItem.SortBy.sortby_nulls ?? \"SORTBY_NULLS_DEFAULT\";\n\n if (sortDir === \"SORTBY_DESC\" && sortNulls === \"SORTBY_NULLS_DEFAULT\") {\n if (sortItem.SortBy.node && is(sortItem.SortBy.node, \"ColumnRef\")) {\n const sortColumnName = getLastColumnRefField(sortItem.SortBy.node);\n\n const hasIsNotNull =\n sortColumnName !== null &&\n whereHasIsNotNull(node.SelectStmt.whereClause, sortColumnName);\n\n if (!hasIsNotNull) {\n nudges.push({\n kind: \"NULLS_FIRST_IN_DESC_ORDER\",\n severity: \"INFO\",\n message:\n \"ORDER BY … DESC sorts NULLs first — add NULLS LAST to push them to the end\",\n location: sortItem.SortBy.node.ColumnRef.location,\n });\n }\n }\n }\n }\n }\n\n // Detect NULL comparison issues (= NULL instead of IS NULL)\n if (is(node, \"A_Expr\")) {\n const isEqualityOp =\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0 &&\n is(node.A_Expr.name[0], \"String\") &&\n (node.A_Expr.name[0].String.sval === \"=\" ||\n node.A_Expr.name[0].String.sval === \"!=\" ||\n node.A_Expr.name[0].String.sval === \"<>\");\n\n if (isEqualityOp) {\n const leftIsNull = isNullConstant(node.A_Expr.lexpr);\n const rightIsNull = isNullConstant(node.A_Expr.rexpr);\n\n if (leftIsNull || rightIsNull) {\n nudges.push({\n kind: \"USE_IS_NULL_NOT_EQUALS\",\n severity: \"WARNING\",\n message: \"Use IS NULL instead of = or != or <> for NULL comparisons\",\n location: node.A_Expr.location,\n });\n }\n }\n\n // Detect LIKE with leading wildcards\n const isLikeOp =\n node.A_Expr.kind === \"AEXPR_LIKE\" || node.A_Expr.kind === \"AEXPR_ILIKE\";\n\n if (isLikeOp && node.A_Expr.rexpr) {\n const patternString = getStringConstantValue(node.A_Expr.rexpr);\n if (patternString && patternString.startsWith(\"%\")) {\n let stringNode: A_Const | undefined;\n if (is(node.A_Expr.rexpr, \"A_Const\")) {\n stringNode = node.A_Expr.rexpr.A_Const;\n }\n nudges.push({\n kind: \"AVOID_LEADING_WILDCARD_LIKE\",\n severity: \"WARNING\",\n message:\n \"Leading wildcard in LIKE/ILIKE prevents index usage — consider a GIN trigram index (pg_trgm) or full-text search\",\n location: stringNode?.location,\n });\n }\n }\n }\n\n // Detect ORDER BY random()\n if (is(node, \"SelectStmt\") && node.SelectStmt.sortClause) {\n for (const sortItem of node.SelectStmt.sortClause) {\n if (\n is(sortItem, \"SortBy\") &&\n sortItem.SortBy.node &&\n is(sortItem.SortBy.node, \"FuncCall\") &&\n sortItem.SortBy.node.FuncCall.funcname?.some(\n (name) => is(name, \"String\") && name.String.sval === \"random\",\n )\n ) {\n nudges.push({\n kind: \"AVOID_ORDER_BY_RANDOM\",\n severity: \"WARNING\",\n message: \"Avoid using ORDER BY random()\",\n location: sortItem.SortBy.node.FuncCall.location,\n });\n }\n }\n }\n\n // Detect DISTINCT usage\n if (is(node, \"SelectStmt\") && node.SelectStmt.distinctClause) {\n nudges.push({\n kind: \"AVOID_DISTINCT_WITHOUT_REASON\",\n severity: \"WARNING\",\n message: \"Avoid using DISTINCT without a reason\",\n });\n }\n\n // Detect cartesian joins (missing JOIN conditions) and functions on columns in ON\n if (is(node, \"JoinExpr\")) {\n if (node.JoinExpr.quals) {\n nudges.push(...findFuncCallsOnColumns(node.JoinExpr.quals));\n } else if (!node.JoinExpr.usingClause) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n });\n }\n }\n\n // Detect multiple tables in FROM without explicit JOINs (old-style cartesian joins)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.fromClause &&\n node.SelectStmt.fromClause.length > 1\n ) {\n // Check if there are multiple RangeVar (tables) directly in FROM clause\n const tables = node.SelectStmt.fromClause.filter((item) =>\n is(item, \"RangeVar\"),\n );\n if (tables.length > 1) {\n // Collect table aliases/names for cross-table equality check\n const tableNames = new Set(\n tables.map(\n (t) => t.RangeVar.alias?.aliasname || t.RangeVar.relname || \"\",\n ),\n );\n\n // Don't fire if WHERE already has a cross-table equality (old-style implicit join)\n if (\n !whereHasCrossTableEquality(node.SelectStmt.whereClause, tableNames)\n ) {\n nudges.push({\n kind: \"MISSING_JOIN_CONDITION\",\n severity: \"WARNING\",\n message: \"Missing JOIN condition\",\n location: tables[1].RangeVar.location,\n });\n }\n }\n }\n\n // Detect too many OR conditions on the same column\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"OR_EXPR\") {\n const orCount = countBoolOrConditions(node);\n if (orCount >= 3 && allOrBranchesReferenceSameColumn(node)) {\n nudges.push({\n kind: \"CONSIDER_IN_INSTEAD_OF_MANY_ORS\",\n severity: \"WARNING\",\n message: \"Consider using IN instead of many ORs\",\n location: node.BoolExpr.location,\n });\n }\n }\n\n // Detect NOT IN (SELECT ...) — prefer NOT EXISTS\n if (is(node, \"BoolExpr\") && node.BoolExpr.boolop === \"NOT_EXPR\") {\n const args = node.BoolExpr.args;\n if (args && args.length === 1) {\n const inner = args[0];\n if (\n isANode(inner) &&\n is(inner, \"SubLink\") &&\n inner.SubLink.subLinkType === \"ANY_SUBLINK\"\n ) {\n nudges.push({\n kind: \"PREFER_NOT_EXISTS_OVER_NOT_IN\",\n severity: \"WARNING\",\n message: \"Prefer NOT EXISTS over NOT IN (SELECT ...)\",\n location: inner.SubLink.location,\n });\n }\n }\n }\n\n // Detect COUNT(column) or COUNT(1) — suggest COUNT(*)\n if (is(node, \"FuncCall\")) {\n const funcName = node.FuncCall.funcname;\n const isCount =\n funcName &&\n funcName.length === 1 &&\n is(funcName[0], \"String\") &&\n funcName[0].String.sval === \"count\";\n\n if (\n isCount &&\n node.FuncCall.args &&\n !node.FuncCall.agg_star &&\n !node.FuncCall.agg_distinct\n ) {\n nudges.push({\n kind: \"PREFER_COUNT_STAR_OVER_COUNT_COLUMN\",\n severity: \"INFO\",\n message:\n \"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.\",\n location: node.FuncCall.location,\n });\n }\n }\n\n // Detect non-aggregate conditions in HAVING clause\n if (is(node, \"SelectStmt\") && node.SelectStmt.havingClause) {\n if (!containsAggregate(node.SelectStmt.havingClause)) {\n const having = node.SelectStmt.havingClause;\n let location: number | undefined;\n if (is(having, \"A_Expr\")) location = having.A_Expr.location;\n else if (is(having, \"BoolExpr\")) location = having.BoolExpr.location;\n\n nudges.push({\n kind: \"PREFER_WHERE_OVER_HAVING_FOR_NON_AGGREGATES\",\n severity: \"INFO\",\n message: \"Non-aggregate condition in HAVING should be in WHERE\",\n location,\n });\n }\n }\n\n // Detect JSONB set-returning functions in WHERE context\n if (is(node, \"FuncCall\")) {\n const inWhereClause = stack.some((item) => item === \"whereClause\");\n if (inWhereClause && node.FuncCall.args) {\n const name = getFuncName(node);\n if (\n name &&\n JSONB_SET_RETURNING_FUNCTIONS.has(name) &&\n containsColumnRef(node.FuncCall.args)\n ) {\n nudges.push({\n kind: \"CONSIDER_JSONB_CONTAINMENT_OPERATOR\",\n severity: \"INFO\",\n message:\n \"JSONB set-returning functions (e.g. jsonb_array_elements) cannot be used as an index access path. If the query checks for containment or key existence, GIN-compatible operators (@>, ?, ?|, ?&, @?, @@) may allow index usage.\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n\n // Too many parameters in a tuple\n if (is(node, \"A_Expr\")) {\n if (node.A_Expr.kind === \"AEXPR_IN\") {\n let list: List | undefined;\n if (node.A_Expr.lexpr && is(node.A_Expr.lexpr, \"List\")) {\n list = node.A_Expr.lexpr.List;\n } else if (node.A_Expr.rexpr && is(node.A_Expr.rexpr, \"List\")) {\n list = node.A_Expr.rexpr.List;\n }\n\n if (list?.items && list.items.length >= 10) {\n nudges.push({\n kind: \"REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY\",\n message:\n \"`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter\",\n severity: \"INFO\",\n location: node.A_Expr.location,\n });\n }\n }\n }\n\n // Detect CASE inside aggregate functions (prefer FILTER)\n if (is(node, \"FuncCall\")) {\n const funcname =\n node.FuncCall.funcname?.[0] &&\n is(node.FuncCall.funcname[0], \"String\") &&\n node.FuncCall.funcname[0].String.sval;\n\n if (\n funcname &&\n [\"sum\", \"count\", \"avg\", \"min\", \"max\"].includes(funcname.toLowerCase())\n ) {\n const firstArg = node.FuncCall.args?.[0];\n if (firstArg && isANode(firstArg) && is(firstArg, \"CaseExpr\")) {\n const caseExpr = firstArg.CaseExpr;\n // Only flag simple CASE expressions (single WHEN clause)\n if (caseExpr.args && caseExpr.args.length === 1) {\n // Check ELSE clause: must be absent, ELSE 0, or ELSE NULL\n const defresult = caseExpr.defresult;\n const isSimpleElse =\n !defresult ||\n (isANode(defresult) &&\n is(defresult, \"A_Const\") &&\n (defresult.A_Const.isnull !== undefined ||\n (defresult.A_Const.ival !== undefined &&\n (defresult.A_Const.ival.ival === 0 ||\n defresult.A_Const.ival.ival === undefined))));\n\n if (isSimpleElse) {\n nudges.push({\n kind: \"PREFER_FILTER_OVER_CASE_IN_AGGREGATE\",\n severity: \"INFO\",\n message:\n \"Use FILTER (WHERE ...) instead of CASE inside aggregate functions\",\n location: node.FuncCall.location,\n });\n }\n }\n }\n }\n }\n\n // Detect UNION without ALL (implicit deduplication)\n if (\n is(node, \"SelectStmt\") &&\n node.SelectStmt.op === \"SETOP_UNION\" &&\n !node.SelectStmt.all\n ) {\n nudges.push({\n kind: \"PREFER_UNION_ALL_OVER_UNION\",\n severity: \"INFO\",\n message:\n \"UNION removes duplicates with an implicit sort — use UNION ALL if deduplication is not needed\",\n });\n }\n\n // Detect COUNT(*)/COUNT(1) compared to 0 or 1 (existence check)\n if (\n is(node, \"A_Expr\") &&\n node.A_Expr.kind === \"AEXPR_OP\" &&\n node.A_Expr.name &&\n node.A_Expr.name.length > 0\n ) {\n const opNode = node.A_Expr.name[0];\n const op = is(opNode, \"String\") ? opNode.String.sval : null;\n\n if (\n op &&\n isExistenceCheckPattern(node.A_Expr.lexpr, node.A_Expr.rexpr, op)\n ) {\n nudges.push({\n kind: \"USE_EXISTS_NOT_COUNT_FOR_EXISTENCE_CHECK\",\n severity: \"INFO\",\n message: \"Use EXISTS instead of COUNT for existence checks\",\n location: node.A_Expr.location,\n });\n }\n }\n\n return nudges;\n}\n\nfunction containsColumnRef(args: unknown[]): boolean {\n // Recursively check if any argument contains a ColumnRef\n for (const arg of args) {\n if (hasColumnRefInNode(arg)) {\n return true;\n }\n }\n return false;\n}\n\nfunction hasColumnRefInNode(node: unknown): boolean {\n if (isANode(node) && is(node, \"ColumnRef\")) {\n return true;\n }\n\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n\n if (Array.isArray(node)) {\n return node.some((item) => hasColumnRefInNode(item));\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return hasColumnRefInNode(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (hasColumnRefInNode(child)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction hasActualTablesInJoin(\n joinExpr: Extract<Node, { JoinExpr: unknown }>,\n): boolean {\n // Check left side of join\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.larg && is(joinExpr.JoinExpr.larg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.larg)) {\n return true;\n }\n }\n\n // Check right side of join\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"RangeVar\")) {\n return true;\n }\n if (joinExpr.JoinExpr.rarg && is(joinExpr.JoinExpr.rarg, \"JoinExpr\")) {\n if (hasActualTablesInJoin(joinExpr.JoinExpr.rarg)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isNullConstant(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return false;\n }\n\n if (isANode(node) && is(node, \"A_Const\")) {\n // Check if it's a NULL constant\n return node.A_Const.isnull !== undefined;\n }\n\n return false;\n}\n\nfunction getStringConstantValue(node: unknown): string | null {\n if (!node || typeof node !== \"object\") {\n return null;\n }\n\n if (isANode(node) && is(node, \"A_Const\") && node.A_Const.sval) {\n return node.A_Const.sval.sval || null;\n }\n\n return null;\n}\n\nconst JSONB_SET_RETURNING_FUNCTIONS = new Set([\n \"jsonb_array_elements\",\n \"json_array_elements\",\n \"jsonb_array_elements_text\",\n \"json_array_elements_text\",\n \"jsonb_each\",\n \"json_each\",\n \"jsonb_each_text\",\n \"json_each_text\",\n]);\n\nfunction getFuncName(\n node: Extract<Node, { FuncCall: unknown }>,\n): string | null {\n const names = node.FuncCall.funcname;\n if (!names || names.length === 0) return null;\n const last = names[names.length - 1];\n if (isANode(last) && is(last, \"String\") && last.String.sval) {\n return last.String.sval;\n }\n return null;\n}\n\nfunction getLastColumnRefField(\n columnRef: Extract<Node, { ColumnRef: unknown }>,\n): string | null {\n const fields = columnRef.ColumnRef.fields;\n if (!fields || fields.length === 0) return null;\n\n const lastField = fields[fields.length - 1];\n if (isANode(lastField) && is(lastField, \"String\")) {\n return lastField.String.sval || null;\n }\n return null;\n}\n\nfunction whereHasIsNotNull(\n whereClause: Node | undefined,\n columnName: string,\n): boolean {\n if (!whereClause) return false;\n\n let found = false;\n Walker.shallowMatch(whereClause, \"NullTest\", (node) => {\n if (\n node.NullTest.nulltesttype === \"IS_NOT_NULL\" &&\n node.NullTest.arg &&\n is(node.NullTest.arg, \"ColumnRef\")\n ) {\n const name = getLastColumnRefField(node.NullTest.arg);\n if (name === columnName) {\n found = true;\n }\n }\n });\n\n return found;\n}\n\nconst AGGREGATE_FUNCTIONS = new Set([\n \"count\",\n \"sum\",\n \"avg\",\n \"min\",\n \"max\",\n \"array_agg\",\n \"string_agg\",\n \"bool_and\",\n \"bool_or\",\n \"every\",\n]);\n\nfunction containsAggregate(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n\n if (Array.isArray(node)) {\n return node.some(containsAggregate);\n }\n\n if (isANode(node) && is(node, \"FuncCall\")) {\n const funcname = node.FuncCall.funcname;\n if (funcname) {\n for (const f of funcname) {\n if (\n isANode(f) &&\n is(f, \"String\") &&\n AGGREGATE_FUNCTIONS.has(f.String.sval?.toLowerCase() ?? \"\")\n ) {\n return true;\n }\n }\n }\n }\n\n if (isANode(node)) {\n const keys = Object.keys(node);\n // @ts-expect-error | nodes don't allow dynamic access but it's the only way to do it\n return containsAggregate(node[keys[0]]);\n }\n\n for (const child of Object.values(node)) {\n if (containsAggregate(child)) return true;\n }\n\n return false;\n}\n\nfunction countBoolOrConditions(\n node: Extract<Node, { BoolExpr: unknown }>,\n): number {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return 1;\n }\n\n let count = 0;\n for (const arg of node.BoolExpr.args) {\n if (\n isANode(arg) &&\n is(arg, \"BoolExpr\") &&\n arg.BoolExpr.boolop === \"OR_EXPR\"\n ) {\n count += countBoolOrConditions(arg);\n } else {\n count += 1;\n }\n }\n\n return count;\n}\n\n/**\n * Check whether every leaf of a top-level OR expression references the same\n * left-hand column (e.g. `status = 'a' OR status = 'b' OR status = 'c'`).\n * Returns false when ORs span different columns — IN rewrite doesn't apply.\n */\nfunction allOrBranchesReferenceSameColumn(\n node: Extract<Node, { BoolExpr: unknown }>,\n): boolean {\n const columns = collectOrLeafColumns(node);\n if (columns.length === 0) return false;\n return columns.every((col) => col === columns[0]);\n}\n\nfunction collectOrLeafColumns(\n node: Extract<Node, { BoolExpr: unknown }>,\n): string[] {\n if (node.BoolExpr.boolop !== \"OR_EXPR\" || !node.BoolExpr.args) {\n return [];\n }\n\n const columns: string[] = [];\n for (const arg of node.BoolExpr.args) {\n if (!isANode(arg)) continue;\n if (is(arg, \"BoolExpr\") && arg.BoolExpr.boolop === \"OR_EXPR\") {\n columns.push(...collectOrLeafColumns(arg));\n } else if (is(arg, \"A_Expr\")) {\n const col = getLeftColumnKey(arg);\n if (col) {\n columns.push(col);\n } else {\n // Non-column comparison — can't be rewritten as IN\n return [];\n }\n } else {\n // Non-A_Expr leaf (e.g. SubLink, BoolExpr AND) — not a simple OR chain\n return [];\n }\n }\n return columns;\n}\n\n/**\n * Get a string key for the left-hand column of an A_Expr equality comparison.\n * For `t1.col = value` returns `\"t1.col\"`, for `col = value` returns `\"col\"`.\n */\nfunction getLeftColumnKey(\n expr: Extract<Node, { A_Expr: unknown }>,\n): string | null {\n if (!expr.A_Expr.lexpr || !isANode(expr.A_Expr.lexpr)) return null;\n if (!is(expr.A_Expr.lexpr, \"ColumnRef\")) return null;\n const fields = expr.A_Expr.lexpr.ColumnRef.fields;\n if (!fields) return null;\n return fields\n .filter(\n (f): f is Extract<Node, { String: unknown }> =>\n isANode(f) && is(f, \"String\"),\n )\n .map((f) => f.String.sval)\n .join(\".\");\n}\n\n/**\n * Check if a WHERE clause contains an equality between columns from different\n * tables (indicating an old-style implicit join condition).\n */\nfunction whereHasCrossTableEquality(\n whereClause: Node | undefined,\n tableNames: Set<string>,\n): boolean {\n if (!whereClause) return false;\n\n if (isANode(whereClause) && is(whereClause, \"A_Expr\")) {\n if (\n whereClause.A_Expr.kind === \"AEXPR_OP\" &&\n whereClause.A_Expr.name?.some(\n (n) => is(n, \"String\") && n.String.sval === \"=\",\n )\n ) {\n const leftTable = getColumnRefTableQualifier(whereClause.A_Expr.lexpr);\n const rightTable = getColumnRefTableQualifier(whereClause.A_Expr.rexpr);\n if (\n leftTable &&\n rightTable &&\n leftTable !== rightTable &&\n tableNames.has(leftTable) &&\n tableNames.has(rightTable)\n ) {\n return true;\n }\n }\n }\n\n // Recurse into AND/OR boolean expressions\n if (isANode(whereClause) && is(whereClause, \"BoolExpr\")) {\n for (const arg of whereClause.BoolExpr.args ?? []) {\n if (isANode(arg) && whereHasCrossTableEquality(arg, tableNames)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Extract the table qualifier (first field) from a ColumnRef node.\n * e.g. `t1.uid` → `\"t1\"`, `uid` → null\n */\nfunction getColumnRefTableQualifier(node: unknown): string | null {\n if (!node || !isANode(node as Node) || !is(node as Node, \"ColumnRef\"))\n return null;\n const fields = (node as Extract<Node, { ColumnRef: unknown }>).ColumnRef\n .fields;\n if (!fields || fields.length < 2) return null;\n const first = fields[0];\n if (isANode(first) && is(first, \"String\")) {\n return first.String.sval || null;\n }\n return null;\n}\n\nfunction isCountFuncCall(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"FuncCall\")) return false;\n const fc = node.FuncCall;\n const isCount =\n fc.funcname?.some((n) => is(n, \"String\") && n.String.sval === \"count\") ??\n false;\n if (!isCount) return false;\n // COUNT(*) has agg_star\n if (fc.agg_star) return true;\n // COUNT(1) or COUNT(literal) — single constant argument\n if (\n fc.args &&\n fc.args.length === 1 &&\n isANode(fc.args[0]) &&\n is(fc.args[0], \"A_Const\")\n ) {\n return true;\n }\n return false;\n}\n\nfunction isSubLinkWithCount(node: unknown): boolean {\n if (!node || typeof node !== \"object\") return false;\n if (!isANode(node) || !is(node, \"SubLink\")) return false;\n const sub = node.SubLink;\n const subselect = sub.subselect;\n if (!subselect || !isANode(subselect) || !is(subselect, \"SelectStmt\"))\n return false;\n const targets = subselect.SelectStmt.targetList;\n if (!targets || targets.length !== 1) return false;\n const target = targets[0];\n if (!isANode(target) || !is(target, \"ResTarget\") || !target.ResTarget.val)\n return false;\n return isCountFuncCall(target.ResTarget.val);\n}\n\nfunction isCountExpression(node: unknown): boolean {\n return isCountFuncCall(node) || isSubLinkWithCount(node);\n}\n\nfunction getIntegerConstantValue(node: unknown): number | null {\n if (!node || typeof node !== \"object\") return null;\n if (!isANode(node) || !is(node, \"A_Const\")) return null;\n if (node.A_Const.ival === undefined) return null;\n // protobuf: ival: {} means 0, ival: { ival: N } means N\n return node.A_Const.ival.ival ?? 0;\n}\n\nfunction isExistenceCheckPattern(\n lexpr: unknown,\n rexpr: unknown,\n op: string,\n): boolean {\n // count_expr > 0, count_expr >= 1, count_expr != 0, count_expr <> 0\n if (isCountExpression(lexpr)) {\n const val = getIntegerConstantValue(rexpr);\n if (val !== null) {\n if (op === \">\" && val === 0) return true;\n if (op === \">=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n // Reversed: 0 < count_expr, 1 <= count_expr, 0 != count_expr\n if (isCountExpression(rexpr)) {\n const val = getIntegerConstantValue(lexpr);\n if (val !== null) {\n if (op === \"<\" && val === 0) return true;\n if (op === \"<=\" && val === 1) return true;\n if ((op === \"!=\" || op === \"<>\") && val === 0) return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/sql/permutations.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create permutations of the array while sorting it from
|
|
5
|
+
* largest permutation to smallest.
|
|
6
|
+
*
|
|
7
|
+
* This is important when generating index permutations as
|
|
8
|
+
* postgres happens to prefer indexes with the latest
|
|
9
|
+
* creation date when the cost of using 2 are the same
|
|
10
|
+
**/
|
|
11
|
+
function permutationsWithDescendingLength(arr) {
|
|
12
|
+
const collected = [];
|
|
13
|
+
function collect(path, rest) {
|
|
14
|
+
for (let i = 0; i < rest.length; i++) {
|
|
15
|
+
const nextRest = [...rest.slice(0, i), ...rest.slice(i + 1)];
|
|
16
|
+
const nextPath = [...path, rest[i]];
|
|
17
|
+
collected.push(nextPath);
|
|
18
|
+
collect(nextPath, nextRest);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
collect([], arr);
|
|
22
|
+
collected.sort((a, b) => b.length - a.length);
|
|
23
|
+
return collected;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
exports.permutationsWithDescendingLength = permutationsWithDescendingLength;
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=permutations.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permutations.cjs","names":[],"sources":["../../src/sql/permutations.ts"],"sourcesContent":["/**\n * Create permutations of the array while sorting it from\n * largest permutation to smallest.\n *\n * This is important when generating index permutations as\n * postgres happens to prefer indexes with the latest\n * creation date when the cost of using 2 are the same\n **/\nexport function permutationsWithDescendingLength<T>(arr: T[]): T[][] {\n const collected: T[][] = [];\n\n function collect(path: T[], rest: T[]): void {\n for (let i = 0; i < rest.length; i++) {\n const nextRest = [...rest.slice(0, i), ...rest.slice(i + 1)];\n const nextPath = [...path, rest[i]];\n collected.push(nextPath);\n collect(nextPath, nextRest);\n }\n }\n\n collect([], arr);\n collected.sort((a, b) => b.length - a.length);\n\n return collected;\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,iCAAoC,KAAiB;CACnE,MAAM,YAAmB,EAAE;CAE3B,SAAS,QAAQ,MAAW,MAAiB;AAC3C,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;GACpC,MAAM,WAAW,CAAC,GAAG,KAAK,MAAM,GAAG,EAAE,EAAE,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;GAC5D,MAAM,WAAW,CAAC,GAAG,MAAM,KAAK,GAAG;AACnC,aAAU,KAAK,SAAS;AACxB,WAAQ,UAAU,SAAS;;;AAI/B,SAAQ,EAAE,EAAE,IAAI;AAChB,WAAU,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;AAE7C,QAAO"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/sql/permutations.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create permutations of the array while sorting it from
|
|
5
|
+
* largest permutation to smallest.
|
|
6
|
+
*
|
|
7
|
+
* This is important when generating index permutations as
|
|
8
|
+
* postgres happens to prefer indexes with the latest
|
|
9
|
+
* creation date when the cost of using 2 are the same
|
|
10
|
+
**/
|
|
11
|
+
function permutationsWithDescendingLength(arr) {
|
|
12
|
+
const collected = [];
|
|
13
|
+
function collect(path, rest) {
|
|
14
|
+
for (let i = 0; i < rest.length; i++) {
|
|
15
|
+
const nextRest = [...rest.slice(0, i), ...rest.slice(i + 1)];
|
|
16
|
+
const nextPath = [...path, rest[i]];
|
|
17
|
+
collected.push(nextPath);
|
|
18
|
+
collect(nextPath, nextRest);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
collect([], arr);
|
|
22
|
+
collected.sort((a, b) => b.length - a.length);
|
|
23
|
+
return collected;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { permutationsWithDescendingLength };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=permutations.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permutations.mjs","names":[],"sources":["../../src/sql/permutations.ts"],"sourcesContent":["/**\n * Create permutations of the array while sorting it from\n * largest permutation to smallest.\n *\n * This is important when generating index permutations as\n * postgres happens to prefer indexes with the latest\n * creation date when the cost of using 2 are the same\n **/\nexport function permutationsWithDescendingLength<T>(arr: T[]): T[][] {\n const collected: T[][] = [];\n\n function collect(path: T[], rest: T[]): void {\n for (let i = 0; i < rest.length; i++) {\n const nextRest = [...rest.slice(0, i), ...rest.slice(i + 1)];\n const nextPath = [...path, rest[i]];\n collected.push(nextPath);\n collect(nextPath, nextRest);\n }\n }\n\n collect([], arr);\n collected.sort((a, b) => b.length - a.length);\n\n return collected;\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,iCAAoC,KAAiB;CACnE,MAAM,YAAmB,EAAE;CAE3B,SAAS,QAAQ,MAAW,MAAiB;AAC3C,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;GACpC,MAAM,WAAW,CAAC,GAAG,KAAK,MAAM,GAAG,EAAE,EAAE,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;GAC5D,MAAM,WAAW,CAAC,GAAG,MAAM,KAAK,GAAG;AACnC,aAAU,KAAK,SAAS;AACxB,WAAQ,UAAU,SAAS;;;AAI/B,SAAQ,EAAE,EAAE,IAAI;AAChB,WAAU,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;AAE7C,QAAO"}
|