@query-doctor/core 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/explain/rewriter.d.ts +4 -0
- package/dist/explain/rewriter.d.ts.map +1 -0
- package/dist/explain/traverse.d.ts +3 -0
- package/dist/explain/traverse.d.ts.map +1 -0
- package/dist/explain/tree.d.ts +73 -0
- package/dist/explain/tree.d.ts.map +1 -0
- package/dist/index.cjs +58 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +58 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +25333 -0
- package/dist/index.mjs.map +1 -0
- package/dist/optimizer/genalgo.test.d.ts +2 -0
- package/dist/optimizer/genalgo.test.d.ts.map +1 -0
- package/dist/optimizer/index-candidate.d.ts +23 -0
- package/dist/optimizer/index-candidate.d.ts.map +1 -0
- package/dist/optimizer/index-shrinker.d.ts +10 -0
- package/dist/optimizer/index-shrinker.d.ts.map +1 -0
- package/dist/optimizer/index-shrinker.test.d.ts +2 -0
- package/dist/optimizer/index-shrinker.test.d.ts.map +1 -0
- package/dist/optimizer/index-tester.d.ts +2 -0
- package/dist/optimizer/index-tester.d.ts.map +1 -0
- package/dist/optimizer/pss-rewriter.test.d.ts +2 -0
- package/dist/optimizer/pss-rewriter.test.d.ts.map +1 -0
- package/dist/sql/analyzer.test.d.ts +2 -0
- package/dist/sql/analyzer.test.d.ts.map +1 -0
- package/dist/sql/builder.test.d.ts +2 -0
- package/dist/sql/builder.test.d.ts.map +1 -0
- package/dist/sql/nudges.d.ts +1 -0
- package/dist/sql/nudges.d.ts.map +1 -1
- package/dist/sql/permutations.test.d.ts +2 -0
- package/dist/sql/permutations.test.d.ts.map +1 -0
- package/dist/sql/pg-identifier.test.d.ts +2 -0
- package/dist/sql/pg-identifier.test.d.ts.map +1 -0
- package/dist/sql/walker.d.ts +2 -1
- package/dist/sql/walker.d.ts.map +1 -1
- package/dist/sql/walker.test.d.ts +2 -0
- package/dist/sql/walker.test.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewriter.d.ts","sourceRoot":"","sources":["../../src/explain/rewriter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAIV,WAAW,EAIZ,MAAM,WAAW,CAAC;AA0CnB,wBAAgB,sBAAsB,CACpC,aAAa,EAAE,qBAAqB,EACpC,KAAK,EAAE,MAAM,GACZ,WAAW,CA8Bb"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traverse.d.ts","sourceRoot":"","sources":["../../src/explain/traverse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,wBAAiB,eAAe,CAC9B,OAAO,EAAE,oBAAoB,GAC5B,SAAS,CAAC,oBAAoB,CAAC,CAUjC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PostgresExplainResult } from "../sql/database.js";
|
|
2
|
+
export type ExplainNodeKind = string;
|
|
3
|
+
export interface ExplainCostMetrics {
|
|
4
|
+
startupCost: number;
|
|
5
|
+
totalCost: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ExplainRowMetrics {
|
|
8
|
+
estimatedRows: number;
|
|
9
|
+
actualRows?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ExplainNodeMetadata {
|
|
12
|
+
raw: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export type DatabaseSystem = "postgresql";
|
|
15
|
+
export interface ExplainPlanMetadata {
|
|
16
|
+
databaseSystem: DatabaseSystem;
|
|
17
|
+
query: string;
|
|
18
|
+
analyzed: boolean;
|
|
19
|
+
planningTime?: number;
|
|
20
|
+
executionTime?: number;
|
|
21
|
+
raw: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface CostBreakdown {
|
|
24
|
+
reason: string;
|
|
25
|
+
cost: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ExplainAlternativePlan {
|
|
28
|
+
kind: ExplainNodeKind;
|
|
29
|
+
indexName?: string;
|
|
30
|
+
pruneReason: string;
|
|
31
|
+
costBreakdown: CostBreakdown[];
|
|
32
|
+
totalCost: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class ExplainNode {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly kind: ExplainNodeKind;
|
|
37
|
+
readonly cost: ExplainCostMetrics;
|
|
38
|
+
readonly rows: ExplainRowMetrics;
|
|
39
|
+
readonly metadata: ExplainNodeMetadata;
|
|
40
|
+
parent: ExplainNode | null;
|
|
41
|
+
private _children;
|
|
42
|
+
private _alternatives;
|
|
43
|
+
get children(): readonly ExplainNode[];
|
|
44
|
+
get alternatives(): readonly ExplainNode[];
|
|
45
|
+
constructor(params: {
|
|
46
|
+
id: string;
|
|
47
|
+
kind: ExplainNodeKind;
|
|
48
|
+
cost: ExplainCostMetrics;
|
|
49
|
+
rows: ExplainRowMetrics;
|
|
50
|
+
metadata: ExplainNodeMetadata;
|
|
51
|
+
});
|
|
52
|
+
addChild(node: ExplainNode): void;
|
|
53
|
+
addAlternative(node: ExplainNode): void;
|
|
54
|
+
root(): ExplainNode;
|
|
55
|
+
depth(): number;
|
|
56
|
+
siblings(): ExplainNode[];
|
|
57
|
+
ancestors(): ExplainNode[];
|
|
58
|
+
find(predicate: (node: ExplainNode) => boolean): ExplainNode | undefined;
|
|
59
|
+
walk(visitor: (node: ExplainNode) => void): void;
|
|
60
|
+
walkBreadth(visitor: (node: ExplainNode) => void): void;
|
|
61
|
+
}
|
|
62
|
+
export declare class ExplainTree {
|
|
63
|
+
root: ExplainNode;
|
|
64
|
+
metadata: ExplainPlanMetadata;
|
|
65
|
+
constructor(root: ExplainNode, metadata: ExplainPlanMetadata);
|
|
66
|
+
static fromPostgresExplain(explainResult: PostgresExplainResult, query: string): ExplainTree;
|
|
67
|
+
replaceFromPostgresExplain(explainResult: PostgresExplainResult, query: string): void;
|
|
68
|
+
find(predicate: (node: ExplainNode) => boolean): ExplainNode | undefined;
|
|
69
|
+
walk(visitor: (node: ExplainNode) => void): void;
|
|
70
|
+
walkBreadth(visitor: (node: ExplainNode) => void): void;
|
|
71
|
+
nodeCount(): number;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=tree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/explain/tree.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC;AAE1C,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,cAAc,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,aAAa,EAAE,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AA+FD,qBAAa,WAAW;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAQ;IAElC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,aAAa,CAAqB;IAE1C,IAAI,QAAQ,IAAI,SAAS,WAAW,EAAE,CAErC;IAED,IAAI,YAAY,IAAI,SAAS,WAAW,EAAE,CAEzC;gBAEW,MAAM,EAAE;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,eAAe,CAAC;QACtB,IAAI,EAAE,kBAAkB,CAAC;QACzB,IAAI,EAAE,iBAAiB,CAAC;QACxB,QAAQ,EAAE,mBAAmB,CAAC;KAC/B;IAQD,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAKjC,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAIvC,IAAI,IAAI,WAAW;IAQnB,KAAK,IAAI,MAAM;IAUf,QAAQ,IAAI,WAAW,EAAE;IAKzB,SAAS,IAAI,WAAW,EAAE;IAU1B,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,GAAG,WAAW,GAAG,SAAS;IASxE,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAOhD,WAAW,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;CAUxD;AAED,qBAAa,WAAW;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,mBAAmB,CAAC;gBAElB,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,mBAAmB;IAK5D,MAAM,CAAC,mBAAmB,CACxB,aAAa,EAAE,qBAAqB,EACpC,KAAK,EAAE,MAAM,GACZ,WAAW;IAOd,0BAA0B,CACxB,aAAa,EAAE,qBAAqB,EACpC,KAAK,EAAE,MAAM,GACZ,IAAI;IAMP,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,GAAG,WAAW,GAAG,SAAS;IAIxE,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAIhD,WAAW,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAIvD,SAAS,IAAI,MAAM;CAKpB"}
|
package/dist/index.cjs
CHANGED
|
@@ -76,12 +76,25 @@ function isANode(node) {
|
|
|
76
76
|
}
|
|
77
77
|
function parseNudges(node, stack) {
|
|
78
78
|
const nudges = [];
|
|
79
|
-
if (is(node, "
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
if (is(node, "SelectStmt")) {
|
|
80
|
+
const star = node.SelectStmt.targetList?.find(
|
|
81
|
+
(target) => {
|
|
82
|
+
if (!(is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "ColumnRef"))) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return target.ResTarget.val.ColumnRef.fields?.some(
|
|
86
|
+
(field) => is(field, "A_Star")
|
|
87
|
+
) ?? false;
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
if (star) {
|
|
91
|
+
nudges.push({
|
|
92
|
+
kind: "AVOID_SELECT_STAR",
|
|
93
|
+
severity: "INFO",
|
|
94
|
+
message: "Avoid using SELECT *",
|
|
95
|
+
location: star.ResTarget.location
|
|
96
|
+
});
|
|
97
|
+
}
|
|
85
98
|
}
|
|
86
99
|
if (is(node, "FuncCall")) {
|
|
87
100
|
const inWhereClause = stack.some((item) => item === "whereClause");
|
|
@@ -91,7 +104,8 @@ function parseNudges(node, stack) {
|
|
|
91
104
|
nudges.push({
|
|
92
105
|
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
93
106
|
severity: "WARNING",
|
|
94
|
-
message: "Avoid using functions on columns in WHERE clause"
|
|
107
|
+
message: "Avoid using functions on columns in WHERE clause",
|
|
108
|
+
location: node.FuncCall.location
|
|
95
109
|
});
|
|
96
110
|
}
|
|
97
111
|
}
|
|
@@ -107,18 +121,24 @@ function parseNudges(node, stack) {
|
|
|
107
121
|
return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
|
|
108
122
|
});
|
|
109
123
|
if (hasActualTables) {
|
|
124
|
+
const firstTable = node.SelectStmt.fromClause.find(
|
|
125
|
+
(item) => is(item, "RangeVar")
|
|
126
|
+
);
|
|
127
|
+
const fromLocation = firstTable?.RangeVar.location;
|
|
110
128
|
if (!node.SelectStmt.whereClause) {
|
|
111
129
|
nudges.push({
|
|
112
130
|
kind: "MISSING_WHERE_CLAUSE",
|
|
113
131
|
severity: "INFO",
|
|
114
|
-
message: "Missing WHERE clause"
|
|
132
|
+
message: "Missing WHERE clause",
|
|
133
|
+
location: fromLocation
|
|
115
134
|
});
|
|
116
135
|
}
|
|
117
136
|
if (!node.SelectStmt.limitCount) {
|
|
118
137
|
nudges.push({
|
|
119
138
|
kind: "MISSING_LIMIT_CLAUSE",
|
|
120
139
|
severity: "INFO",
|
|
121
|
-
message: "Missing LIMIT clause"
|
|
140
|
+
message: "Missing LIMIT clause",
|
|
141
|
+
location: fromLocation
|
|
122
142
|
});
|
|
123
143
|
}
|
|
124
144
|
}
|
|
@@ -134,7 +154,8 @@ function parseNudges(node, stack) {
|
|
|
134
154
|
nudges.push({
|
|
135
155
|
kind: "USE_IS_NULL_NOT_EQUALS",
|
|
136
156
|
severity: "WARNING",
|
|
137
|
-
message: "Use IS NULL instead of = or != or <> for NULL comparisons"
|
|
157
|
+
message: "Use IS NULL instead of = or != or <> for NULL comparisons",
|
|
158
|
+
location: node.A_Expr.location
|
|
138
159
|
});
|
|
139
160
|
}
|
|
140
161
|
}
|
|
@@ -142,10 +163,15 @@ function parseNudges(node, stack) {
|
|
|
142
163
|
if (isLikeOp && node.A_Expr.rexpr) {
|
|
143
164
|
const patternString = getStringConstantValue(node.A_Expr.rexpr);
|
|
144
165
|
if (patternString && patternString.startsWith("%")) {
|
|
166
|
+
let stringNode;
|
|
167
|
+
if (is(node.A_Expr.rexpr, "A_Const")) {
|
|
168
|
+
stringNode = node.A_Expr.rexpr.A_Const;
|
|
169
|
+
}
|
|
145
170
|
nudges.push({
|
|
146
171
|
kind: "AVOID_LEADING_WILDCARD_LIKE",
|
|
147
172
|
severity: "WARNING",
|
|
148
|
-
message: "Avoid using LIKE with leading wildcards"
|
|
173
|
+
message: "Avoid using LIKE with leading wildcards",
|
|
174
|
+
location: stringNode?.location
|
|
149
175
|
});
|
|
150
176
|
}
|
|
151
177
|
}
|
|
@@ -167,14 +193,15 @@ function parseNudges(node, stack) {
|
|
|
167
193
|
}
|
|
168
194
|
}
|
|
169
195
|
if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
|
|
170
|
-
const
|
|
196
|
+
const tables = node.SelectStmt.fromClause.filter(
|
|
171
197
|
(item) => is(item, "RangeVar")
|
|
172
|
-
)
|
|
173
|
-
if (
|
|
198
|
+
);
|
|
199
|
+
if (tables.length > 1) {
|
|
174
200
|
nudges.push({
|
|
175
201
|
kind: "MISSING_JOIN_CONDITION",
|
|
176
202
|
severity: "WARNING",
|
|
177
|
-
message: "Missing JOIN condition"
|
|
203
|
+
message: "Missing JOIN condition",
|
|
204
|
+
location: tables[1].RangeVar.location
|
|
178
205
|
});
|
|
179
206
|
}
|
|
180
207
|
}
|
|
@@ -184,7 +211,8 @@ function parseNudges(node, stack) {
|
|
|
184
211
|
nudges.push({
|
|
185
212
|
kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
|
|
186
213
|
severity: "WARNING",
|
|
187
|
-
message: "Consider using IN instead of many ORs"
|
|
214
|
+
message: "Consider using IN instead of many ORs",
|
|
215
|
+
location: node.BoolExpr.location
|
|
188
216
|
});
|
|
189
217
|
}
|
|
190
218
|
}
|
|
@@ -200,7 +228,8 @@ function parseNudges(node, stack) {
|
|
|
200
228
|
nudges.push({
|
|
201
229
|
kind: "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY",
|
|
202
230
|
message: "`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter",
|
|
203
|
-
severity: "INFO"
|
|
231
|
+
severity: "INFO",
|
|
232
|
+
location: node.A_Expr.location
|
|
204
233
|
});
|
|
205
234
|
}
|
|
206
235
|
}
|
|
@@ -314,7 +343,7 @@ var Walker = class _Walker {
|
|
|
314
343
|
this.seenReferences = /* @__PURE__ */ new Map();
|
|
315
344
|
this.shadowedAliases = [];
|
|
316
345
|
this.nudges = [];
|
|
317
|
-
_Walker.traverse(root,
|
|
346
|
+
_Walker.traverse(root, (node, stack) => {
|
|
318
347
|
const nodeNudges = parseNudges(node, stack);
|
|
319
348
|
this.nudges = [...this.nudges, ...nodeNudges];
|
|
320
349
|
if (is2(node, "CommonTableExpr")) {
|
|
@@ -507,7 +536,10 @@ var Walker = class _Walker {
|
|
|
507
536
|
}
|
|
508
537
|
this.highlights.push(ref);
|
|
509
538
|
}
|
|
510
|
-
static traverse(node,
|
|
539
|
+
static traverse(node, callback) {
|
|
540
|
+
_Walker.doTraverse(node, [], callback);
|
|
541
|
+
}
|
|
542
|
+
static doTraverse(node, stack, callback) {
|
|
511
543
|
if (isANode2(node)) {
|
|
512
544
|
callback(node, [...stack, getNodeKind(node)]);
|
|
513
545
|
}
|
|
@@ -517,15 +549,19 @@ var Walker = class _Walker {
|
|
|
517
549
|
if (Array.isArray(node)) {
|
|
518
550
|
for (const item of node) {
|
|
519
551
|
if (isANode2(item)) {
|
|
520
|
-
_Walker.
|
|
552
|
+
_Walker.doTraverse(item, stack, callback);
|
|
521
553
|
}
|
|
522
554
|
}
|
|
523
555
|
} else if (isANode2(node)) {
|
|
524
556
|
const keys = Object.keys(node);
|
|
525
|
-
_Walker.
|
|
557
|
+
_Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);
|
|
526
558
|
} else {
|
|
527
559
|
for (const [key, child] of Object.entries(node)) {
|
|
528
|
-
_Walker.
|
|
560
|
+
_Walker.doTraverse(
|
|
561
|
+
child,
|
|
562
|
+
[...stack, key],
|
|
563
|
+
callback
|
|
564
|
+
);
|
|
529
565
|
}
|
|
530
566
|
}
|
|
531
567
|
}
|