@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.
Files changed (39) hide show
  1. package/dist/explain/rewriter.d.ts +4 -0
  2. package/dist/explain/rewriter.d.ts.map +1 -0
  3. package/dist/explain/traverse.d.ts +3 -0
  4. package/dist/explain/traverse.d.ts.map +1 -0
  5. package/dist/explain/tree.d.ts +73 -0
  6. package/dist/explain/tree.d.ts.map +1 -0
  7. package/dist/index.cjs +58 -22
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.js +58 -22
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +25333 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/dist/optimizer/genalgo.test.d.ts +2 -0
  14. package/dist/optimizer/genalgo.test.d.ts.map +1 -0
  15. package/dist/optimizer/index-candidate.d.ts +23 -0
  16. package/dist/optimizer/index-candidate.d.ts.map +1 -0
  17. package/dist/optimizer/index-shrinker.d.ts +10 -0
  18. package/dist/optimizer/index-shrinker.d.ts.map +1 -0
  19. package/dist/optimizer/index-shrinker.test.d.ts +2 -0
  20. package/dist/optimizer/index-shrinker.test.d.ts.map +1 -0
  21. package/dist/optimizer/index-tester.d.ts +2 -0
  22. package/dist/optimizer/index-tester.d.ts.map +1 -0
  23. package/dist/optimizer/pss-rewriter.test.d.ts +2 -0
  24. package/dist/optimizer/pss-rewriter.test.d.ts.map +1 -0
  25. package/dist/sql/analyzer.test.d.ts +2 -0
  26. package/dist/sql/analyzer.test.d.ts.map +1 -0
  27. package/dist/sql/builder.test.d.ts +2 -0
  28. package/dist/sql/builder.test.d.ts.map +1 -0
  29. package/dist/sql/nudges.d.ts +1 -0
  30. package/dist/sql/nudges.d.ts.map +1 -1
  31. package/dist/sql/permutations.test.d.ts +2 -0
  32. package/dist/sql/permutations.test.d.ts.map +1 -0
  33. package/dist/sql/pg-identifier.test.d.ts +2 -0
  34. package/dist/sql/pg-identifier.test.d.ts.map +1 -0
  35. package/dist/sql/walker.d.ts +2 -1
  36. package/dist/sql/walker.d.ts.map +1 -1
  37. package/dist/sql/walker.test.d.ts +2 -0
  38. package/dist/sql/walker.test.d.ts.map +1 -0
  39. package/package.json +1 -1
@@ -0,0 +1,4 @@
1
+ import type { PostgresExplainResult } from "../sql/database.js";
2
+ import type { ExplainPlan } from "./tree.js";
3
+ export declare function convertPostgresExplain(explainResult: PostgresExplainResult, query: string): ExplainPlan;
4
+ //# sourceMappingURL=rewriter.d.ts.map
@@ -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,3 @@
1
+ import type { PostgresExplainStage } from "../sql/database";
2
+ export declare function traverseExplain(explain: PostgresExplainStage): Generator<PostgresExplainStage>;
3
+ //# sourceMappingURL=traverse.d.ts.map
@@ -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":"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"}
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, "A_Star")) {
80
- nudges.push({
81
- kind: "AVOID_SELECT_STAR",
82
- severity: "INFO",
83
- message: "Avoid using SELECT *"
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 tableCount = node.SelectStmt.fromClause.filter(
196
+ const tables = node.SelectStmt.fromClause.filter(
171
197
  (item) => is(item, "RangeVar")
172
- ).length;
173
- if (tableCount > 1) {
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, [], (node, stack) => {
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, stack, callback) {
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.traverse(item, stack, callback);
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.traverse(node[keys[0]], [...stack, getNodeKind(node)], callback);
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.traverse(child, [...stack, key], callback);
560
+ _Walker.doTraverse(
561
+ child,
562
+ [...stack, key],
563
+ callback
564
+ );
529
565
  }
530
566
  }
531
567
  }