@query-doctor/core 0.3.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 +202 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +202 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +25333 -0
- package/dist/index.mjs.map +1 -0
- package/dist/optimizer/genalgo.d.ts +7 -0
- package/dist/optimizer/genalgo.d.ts.map +1 -1
- 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/optimizer/statistics.d.ts +9 -0
- package/dist/optimizer/statistics.d.ts.map +1 -1
- package/dist/sql/analyzer.d.ts +2 -0
- package/dist/sql/analyzer.d.ts.map +1 -1
- 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":"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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, "
|
|
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")) {
|
|
@@ -392,6 +421,18 @@ var Walker = class _Walker {
|
|
|
392
421
|
}
|
|
393
422
|
}
|
|
394
423
|
}
|
|
424
|
+
if (is2(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
|
|
425
|
+
const opName = node.A_Expr.name?.[0] && is2(node.A_Expr.name[0], "String") && node.A_Expr.name[0].String.sval;
|
|
426
|
+
if (opName && (opName === "@>" || opName === "?" || opName === "?|" || opName === "?&")) {
|
|
427
|
+
const jsonbOperator = opName;
|
|
428
|
+
if (node.A_Expr.lexpr && is2(node.A_Expr.lexpr, "ColumnRef")) {
|
|
429
|
+
this.add(node.A_Expr.lexpr, { jsonbOperator });
|
|
430
|
+
}
|
|
431
|
+
if (node.A_Expr.rexpr && is2(node.A_Expr.rexpr, "ColumnRef")) {
|
|
432
|
+
this.add(node.A_Expr.rexpr, { jsonbOperator });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
395
436
|
if (is2(node, "ColumnRef")) {
|
|
396
437
|
for (let i = 0; i < stack.length; i++) {
|
|
397
438
|
const inReturningList = stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef";
|
|
@@ -490,9 +531,15 @@ var Walker = class _Walker {
|
|
|
490
531
|
if (options?.where) {
|
|
491
532
|
ref.where = options.where;
|
|
492
533
|
}
|
|
534
|
+
if (options?.jsonbOperator) {
|
|
535
|
+
ref.jsonbOperator = options.jsonbOperator;
|
|
536
|
+
}
|
|
493
537
|
this.highlights.push(ref);
|
|
494
538
|
}
|
|
495
|
-
static traverse(node,
|
|
539
|
+
static traverse(node, callback) {
|
|
540
|
+
_Walker.doTraverse(node, [], callback);
|
|
541
|
+
}
|
|
542
|
+
static doTraverse(node, stack, callback) {
|
|
496
543
|
if (isANode2(node)) {
|
|
497
544
|
callback(node, [...stack, getNodeKind(node)]);
|
|
498
545
|
}
|
|
@@ -502,15 +549,19 @@ var Walker = class _Walker {
|
|
|
502
549
|
if (Array.isArray(node)) {
|
|
503
550
|
for (const item of node) {
|
|
504
551
|
if (isANode2(item)) {
|
|
505
|
-
_Walker.
|
|
552
|
+
_Walker.doTraverse(item, stack, callback);
|
|
506
553
|
}
|
|
507
554
|
}
|
|
508
555
|
} else if (isANode2(node)) {
|
|
509
556
|
const keys = Object.keys(node);
|
|
510
|
-
_Walker.
|
|
557
|
+
_Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);
|
|
511
558
|
} else {
|
|
512
559
|
for (const [key, child] of Object.entries(node)) {
|
|
513
|
-
_Walker.
|
|
560
|
+
_Walker.doTraverse(
|
|
561
|
+
child,
|
|
562
|
+
[...stack, key],
|
|
563
|
+
callback
|
|
564
|
+
);
|
|
514
565
|
}
|
|
515
566
|
}
|
|
516
567
|
}
|
|
@@ -654,6 +705,9 @@ var Analyzer = class {
|
|
|
654
705
|
if (colReference.where) {
|
|
655
706
|
index.where = colReference.where;
|
|
656
707
|
}
|
|
708
|
+
if (colReference.jsonbOperator) {
|
|
709
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
710
|
+
}
|
|
657
711
|
addIndex(index);
|
|
658
712
|
}
|
|
659
713
|
} else if (tableReference) {
|
|
@@ -675,6 +729,9 @@ var Analyzer = class {
|
|
|
675
729
|
if (colReference.where) {
|
|
676
730
|
index.where = colReference.where;
|
|
677
731
|
}
|
|
732
|
+
if (colReference.jsonbOperator) {
|
|
733
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
734
|
+
}
|
|
678
735
|
addIndex(index);
|
|
679
736
|
}
|
|
680
737
|
} else if (fullReference) {
|
|
@@ -693,6 +750,9 @@ var Analyzer = class {
|
|
|
693
750
|
if (colReference.where) {
|
|
694
751
|
index.where = colReference.where;
|
|
695
752
|
}
|
|
753
|
+
if (colReference.jsonbOperator) {
|
|
754
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
755
|
+
}
|
|
696
756
|
addIndex(index);
|
|
697
757
|
} else {
|
|
698
758
|
console.error(
|
|
@@ -815,7 +875,7 @@ async function dropIndex(tx, index) {
|
|
|
815
875
|
|
|
816
876
|
// src/sql/indexes.ts
|
|
817
877
|
function isIndexSupported(index) {
|
|
818
|
-
return index.index_type === "btree";
|
|
878
|
+
return index.index_type === "btree" || index.index_type === "gin";
|
|
819
879
|
}
|
|
820
880
|
function isIndexProbablyDroppable(index) {
|
|
821
881
|
return !index.is_primary && !index.is_unique;
|
|
@@ -1297,8 +1357,10 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1297
1357
|
* Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
|
|
1298
1358
|
**/
|
|
1299
1359
|
indexesToCreate(rootCandidates) {
|
|
1300
|
-
const
|
|
1360
|
+
const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
|
|
1361
|
+
const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
|
|
1301
1362
|
const nextStage = [];
|
|
1363
|
+
const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
|
|
1302
1364
|
for (const permutation of permutedIndexes.values()) {
|
|
1303
1365
|
const { table: rawTable, schema: rawSchema, columns } = permutation;
|
|
1304
1366
|
const permutations = permutationsWithDescendingLength(columns);
|
|
@@ -1323,6 +1385,41 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1323
1385
|
});
|
|
1324
1386
|
}
|
|
1325
1387
|
}
|
|
1388
|
+
const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
|
|
1389
|
+
for (const group of ginGroups.values()) {
|
|
1390
|
+
const { schema: rawSchema, table: rawTable, column, operators } = group;
|
|
1391
|
+
const schema = PgIdentifier.fromString(rawSchema);
|
|
1392
|
+
const table = PgIdentifier.fromString(rawTable);
|
|
1393
|
+
const needsKeyExistence = operators.some(
|
|
1394
|
+
(op) => op === "?" || op === "?|" || op === "?&"
|
|
1395
|
+
);
|
|
1396
|
+
const opclass = needsKeyExistence ? void 0 : "jsonb_path_ops";
|
|
1397
|
+
const existingGin = this.ginIndexAlreadyExists(table.toString(), column);
|
|
1398
|
+
if (existingGin) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const indexName = this.indexName();
|
|
1402
|
+
const candidate = {
|
|
1403
|
+
schema: rawSchema,
|
|
1404
|
+
table: rawTable,
|
|
1405
|
+
column
|
|
1406
|
+
};
|
|
1407
|
+
const definition = this.toGinDefinition({
|
|
1408
|
+
table,
|
|
1409
|
+
schema,
|
|
1410
|
+
column: PgIdentifier.fromString(column),
|
|
1411
|
+
opclass
|
|
1412
|
+
});
|
|
1413
|
+
nextStage.push({
|
|
1414
|
+
name: indexName,
|
|
1415
|
+
schema: schema.toString(),
|
|
1416
|
+
table: table.toString(),
|
|
1417
|
+
columns: [candidate],
|
|
1418
|
+
definition,
|
|
1419
|
+
indexMethod: "gin",
|
|
1420
|
+
opclass
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1326
1423
|
return nextStage;
|
|
1327
1424
|
}
|
|
1328
1425
|
toDefinition({
|
|
@@ -1357,6 +1454,47 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1357
1454
|
const colored = make(import_colorette2.green, import_colorette2.yellow, import_colorette2.magenta, import_colorette2.blue);
|
|
1358
1455
|
return { raw, colored };
|
|
1359
1456
|
}
|
|
1457
|
+
toGinDefinition({
|
|
1458
|
+
schema,
|
|
1459
|
+
table,
|
|
1460
|
+
column,
|
|
1461
|
+
opclass
|
|
1462
|
+
}) {
|
|
1463
|
+
let fullyQualifiedTable;
|
|
1464
|
+
if (schema.toString() === "public") {
|
|
1465
|
+
fullyQualifiedTable = table;
|
|
1466
|
+
} else {
|
|
1467
|
+
fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
|
|
1468
|
+
}
|
|
1469
|
+
const opclassSuffix = opclass ? ` ${opclass}` : "";
|
|
1470
|
+
return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
|
|
1471
|
+
}
|
|
1472
|
+
groupGinCandidatesByColumn(candidates) {
|
|
1473
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1474
|
+
for (const c of candidates) {
|
|
1475
|
+
if (!c.jsonbOperator) continue;
|
|
1476
|
+
const key = `${c.schema}.${c.table}.${c.column}`;
|
|
1477
|
+
const existing = groups.get(key);
|
|
1478
|
+
if (existing) {
|
|
1479
|
+
if (!existing.operators.includes(c.jsonbOperator)) {
|
|
1480
|
+
existing.operators.push(c.jsonbOperator);
|
|
1481
|
+
}
|
|
1482
|
+
} else {
|
|
1483
|
+
groups.set(key, {
|
|
1484
|
+
schema: c.schema,
|
|
1485
|
+
table: c.table,
|
|
1486
|
+
column: c.column,
|
|
1487
|
+
operators: [c.jsonbOperator]
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return groups;
|
|
1492
|
+
}
|
|
1493
|
+
ginIndexAlreadyExists(table, column) {
|
|
1494
|
+
return this.existingIndexes.find(
|
|
1495
|
+
(index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column)
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1360
1498
|
/**
|
|
1361
1499
|
* Drop indexes that can be dropped. Ignore the ones that can't
|
|
1362
1500
|
*/
|
|
@@ -1673,6 +1811,21 @@ var _Statistics = class _Statistics {
|
|
|
1673
1811
|
}
|
|
1674
1812
|
return "text";
|
|
1675
1813
|
}
|
|
1814
|
+
/**
|
|
1815
|
+
* PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays
|
|
1816
|
+
* for columns with array types (e.g. text[], int4[]). These create
|
|
1817
|
+
* multidimensional arrays that can be "ragged" (sub-arrays with different
|
|
1818
|
+
* lengths). jsonb_to_recordset can't reconstruct ragged multidimensional
|
|
1819
|
+
* arrays from JSON, so we need to drop these values.
|
|
1820
|
+
*/
|
|
1821
|
+
static safeStavalues(values) {
|
|
1822
|
+
if (!values || values.length === 0) return values;
|
|
1823
|
+
if (values.some((v) => Array.isArray(v))) {
|
|
1824
|
+
console.warn("Discarding ragged multidimensional stavalues array");
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
return values;
|
|
1828
|
+
}
|
|
1676
1829
|
async restoreStats17(tx) {
|
|
1677
1830
|
const warnings = {
|
|
1678
1831
|
tablesNotInExports: [],
|
|
@@ -1724,16 +1877,26 @@ var _Statistics = class _Statistics {
|
|
|
1724
1877
|
stanumbers3: stats.stanumbers3,
|
|
1725
1878
|
stanumbers4: stats.stanumbers4,
|
|
1726
1879
|
stanumbers5: stats.stanumbers5,
|
|
1727
|
-
stavalues1: stats.stavalues1,
|
|
1728
|
-
stavalues2: stats.stavalues2,
|
|
1729
|
-
stavalues3: stats.stavalues3,
|
|
1730
|
-
stavalues4: stats.stavalues4,
|
|
1731
|
-
stavalues5: stats.stavalues5,
|
|
1732
|
-
_value_type1: this.stavalueKind(
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1880
|
+
stavalues1: _Statistics.safeStavalues(stats.stavalues1),
|
|
1881
|
+
stavalues2: _Statistics.safeStavalues(stats.stavalues2),
|
|
1882
|
+
stavalues3: _Statistics.safeStavalues(stats.stavalues3),
|
|
1883
|
+
stavalues4: _Statistics.safeStavalues(stats.stavalues4),
|
|
1884
|
+
stavalues5: _Statistics.safeStavalues(stats.stavalues5),
|
|
1885
|
+
_value_type1: this.stavalueKind(
|
|
1886
|
+
_Statistics.safeStavalues(stats.stavalues1)
|
|
1887
|
+
),
|
|
1888
|
+
_value_type2: this.stavalueKind(
|
|
1889
|
+
_Statistics.safeStavalues(stats.stavalues2)
|
|
1890
|
+
),
|
|
1891
|
+
_value_type3: this.stavalueKind(
|
|
1892
|
+
_Statistics.safeStavalues(stats.stavalues3)
|
|
1893
|
+
),
|
|
1894
|
+
_value_type4: this.stavalueKind(
|
|
1895
|
+
_Statistics.safeStavalues(stats.stavalues4)
|
|
1896
|
+
),
|
|
1897
|
+
_value_type5: this.stavalueKind(
|
|
1898
|
+
_Statistics.safeStavalues(stats.stavalues5)
|
|
1899
|
+
)
|
|
1737
1900
|
});
|
|
1738
1901
|
}
|
|
1739
1902
|
}
|
|
@@ -2149,14 +2312,16 @@ var _Statistics = class _Statistics {
|
|
|
2149
2312
|
CASE
|
|
2150
2313
|
WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
|
|
2151
2314
|
ELSE 'ASC'
|
|
2152
|
-
END
|
|
2315
|
+
END,
|
|
2316
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2153
2317
|
-- Handle expressions
|
|
2154
2318
|
ELSE
|
|
2155
2319
|
json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
|
|
2156
2320
|
CASE
|
|
2157
2321
|
WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
|
|
2158
2322
|
ELSE 'ASC'
|
|
2159
|
-
END
|
|
2323
|
+
END,
|
|
2324
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2160
2325
|
END
|
|
2161
2326
|
ORDER BY array_position(ix.indkey, k.attnum)
|
|
2162
2327
|
) AS index_columns
|
|
@@ -2168,6 +2333,7 @@ var _Statistics = class _Statistics {
|
|
|
2168
2333
|
JOIN pg_am am ON i.relam = am.oid
|
|
2169
2334
|
LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
|
|
2170
2335
|
LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
|
|
2336
|
+
LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]
|
|
2171
2337
|
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
2172
2338
|
WHERE
|
|
2173
2339
|
n.nspname not like 'pg_%' and
|