@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
package/dist/index.js
CHANGED
|
@@ -27,12 +27,25 @@ function isANode(node) {
|
|
|
27
27
|
}
|
|
28
28
|
function parseNudges(node, stack) {
|
|
29
29
|
const nudges = [];
|
|
30
|
-
if (is(node, "
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
if (is(node, "SelectStmt")) {
|
|
31
|
+
const star = node.SelectStmt.targetList?.find(
|
|
32
|
+
(target) => {
|
|
33
|
+
if (!(is(target, "ResTarget") && target.ResTarget.val && is(target.ResTarget.val, "ColumnRef"))) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return target.ResTarget.val.ColumnRef.fields?.some(
|
|
37
|
+
(field) => is(field, "A_Star")
|
|
38
|
+
) ?? false;
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
if (star) {
|
|
42
|
+
nudges.push({
|
|
43
|
+
kind: "AVOID_SELECT_STAR",
|
|
44
|
+
severity: "INFO",
|
|
45
|
+
message: "Avoid using SELECT *",
|
|
46
|
+
location: star.ResTarget.location
|
|
47
|
+
});
|
|
48
|
+
}
|
|
36
49
|
}
|
|
37
50
|
if (is(node, "FuncCall")) {
|
|
38
51
|
const inWhereClause = stack.some((item) => item === "whereClause");
|
|
@@ -42,7 +55,8 @@ function parseNudges(node, stack) {
|
|
|
42
55
|
nudges.push({
|
|
43
56
|
kind: "AVOID_FUNCTIONS_ON_COLUMNS_IN_WHERE",
|
|
44
57
|
severity: "WARNING",
|
|
45
|
-
message: "Avoid using functions on columns in WHERE clause"
|
|
58
|
+
message: "Avoid using functions on columns in WHERE clause",
|
|
59
|
+
location: node.FuncCall.location
|
|
46
60
|
});
|
|
47
61
|
}
|
|
48
62
|
}
|
|
@@ -58,18 +72,24 @@ function parseNudges(node, stack) {
|
|
|
58
72
|
return is(fromItem, "RangeVar") || is(fromItem, "JoinExpr") && hasActualTablesInJoin(fromItem);
|
|
59
73
|
});
|
|
60
74
|
if (hasActualTables) {
|
|
75
|
+
const firstTable = node.SelectStmt.fromClause.find(
|
|
76
|
+
(item) => is(item, "RangeVar")
|
|
77
|
+
);
|
|
78
|
+
const fromLocation = firstTable?.RangeVar.location;
|
|
61
79
|
if (!node.SelectStmt.whereClause) {
|
|
62
80
|
nudges.push({
|
|
63
81
|
kind: "MISSING_WHERE_CLAUSE",
|
|
64
82
|
severity: "INFO",
|
|
65
|
-
message: "Missing WHERE clause"
|
|
83
|
+
message: "Missing WHERE clause",
|
|
84
|
+
location: fromLocation
|
|
66
85
|
});
|
|
67
86
|
}
|
|
68
87
|
if (!node.SelectStmt.limitCount) {
|
|
69
88
|
nudges.push({
|
|
70
89
|
kind: "MISSING_LIMIT_CLAUSE",
|
|
71
90
|
severity: "INFO",
|
|
72
|
-
message: "Missing LIMIT clause"
|
|
91
|
+
message: "Missing LIMIT clause",
|
|
92
|
+
location: fromLocation
|
|
73
93
|
});
|
|
74
94
|
}
|
|
75
95
|
}
|
|
@@ -85,7 +105,8 @@ function parseNudges(node, stack) {
|
|
|
85
105
|
nudges.push({
|
|
86
106
|
kind: "USE_IS_NULL_NOT_EQUALS",
|
|
87
107
|
severity: "WARNING",
|
|
88
|
-
message: "Use IS NULL instead of = or != or <> for NULL comparisons"
|
|
108
|
+
message: "Use IS NULL instead of = or != or <> for NULL comparisons",
|
|
109
|
+
location: node.A_Expr.location
|
|
89
110
|
});
|
|
90
111
|
}
|
|
91
112
|
}
|
|
@@ -93,10 +114,15 @@ function parseNudges(node, stack) {
|
|
|
93
114
|
if (isLikeOp && node.A_Expr.rexpr) {
|
|
94
115
|
const patternString = getStringConstantValue(node.A_Expr.rexpr);
|
|
95
116
|
if (patternString && patternString.startsWith("%")) {
|
|
117
|
+
let stringNode;
|
|
118
|
+
if (is(node.A_Expr.rexpr, "A_Const")) {
|
|
119
|
+
stringNode = node.A_Expr.rexpr.A_Const;
|
|
120
|
+
}
|
|
96
121
|
nudges.push({
|
|
97
122
|
kind: "AVOID_LEADING_WILDCARD_LIKE",
|
|
98
123
|
severity: "WARNING",
|
|
99
|
-
message: "Avoid using LIKE with leading wildcards"
|
|
124
|
+
message: "Avoid using LIKE with leading wildcards",
|
|
125
|
+
location: stringNode?.location
|
|
100
126
|
});
|
|
101
127
|
}
|
|
102
128
|
}
|
|
@@ -118,14 +144,15 @@ function parseNudges(node, stack) {
|
|
|
118
144
|
}
|
|
119
145
|
}
|
|
120
146
|
if (is(node, "SelectStmt") && node.SelectStmt.fromClause && node.SelectStmt.fromClause.length > 1) {
|
|
121
|
-
const
|
|
147
|
+
const tables = node.SelectStmt.fromClause.filter(
|
|
122
148
|
(item) => is(item, "RangeVar")
|
|
123
|
-
)
|
|
124
|
-
if (
|
|
149
|
+
);
|
|
150
|
+
if (tables.length > 1) {
|
|
125
151
|
nudges.push({
|
|
126
152
|
kind: "MISSING_JOIN_CONDITION",
|
|
127
153
|
severity: "WARNING",
|
|
128
|
-
message: "Missing JOIN condition"
|
|
154
|
+
message: "Missing JOIN condition",
|
|
155
|
+
location: tables[1].RangeVar.location
|
|
129
156
|
});
|
|
130
157
|
}
|
|
131
158
|
}
|
|
@@ -135,7 +162,8 @@ function parseNudges(node, stack) {
|
|
|
135
162
|
nudges.push({
|
|
136
163
|
kind: "CONSIDER_IN_INSTEAD_OF_MANY_ORS",
|
|
137
164
|
severity: "WARNING",
|
|
138
|
-
message: "Consider using IN instead of many ORs"
|
|
165
|
+
message: "Consider using IN instead of many ORs",
|
|
166
|
+
location: node.BoolExpr.location
|
|
139
167
|
});
|
|
140
168
|
}
|
|
141
169
|
}
|
|
@@ -151,7 +179,8 @@ function parseNudges(node, stack) {
|
|
|
151
179
|
nudges.push({
|
|
152
180
|
kind: "REPLACE_LARGE_IN_TUPLE_WITH_ANY_ARRAY",
|
|
153
181
|
message: "`in (...)` queries with large tuples can often be replaced with `= ANY($1)` using a single parameter",
|
|
154
|
-
severity: "INFO"
|
|
182
|
+
severity: "INFO",
|
|
183
|
+
location: node.A_Expr.location
|
|
155
184
|
});
|
|
156
185
|
}
|
|
157
186
|
}
|
|
@@ -265,7 +294,7 @@ var Walker = class _Walker {
|
|
|
265
294
|
this.seenReferences = /* @__PURE__ */ new Map();
|
|
266
295
|
this.shadowedAliases = [];
|
|
267
296
|
this.nudges = [];
|
|
268
|
-
_Walker.traverse(root,
|
|
297
|
+
_Walker.traverse(root, (node, stack) => {
|
|
269
298
|
const nodeNudges = parseNudges(node, stack);
|
|
270
299
|
this.nudges = [...this.nudges, ...nodeNudges];
|
|
271
300
|
if (is2(node, "CommonTableExpr")) {
|
|
@@ -343,6 +372,18 @@ var Walker = class _Walker {
|
|
|
343
372
|
}
|
|
344
373
|
}
|
|
345
374
|
}
|
|
375
|
+
if (is2(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
|
|
376
|
+
const opName = node.A_Expr.name?.[0] && is2(node.A_Expr.name[0], "String") && node.A_Expr.name[0].String.sval;
|
|
377
|
+
if (opName && (opName === "@>" || opName === "?" || opName === "?|" || opName === "?&")) {
|
|
378
|
+
const jsonbOperator = opName;
|
|
379
|
+
if (node.A_Expr.lexpr && is2(node.A_Expr.lexpr, "ColumnRef")) {
|
|
380
|
+
this.add(node.A_Expr.lexpr, { jsonbOperator });
|
|
381
|
+
}
|
|
382
|
+
if (node.A_Expr.rexpr && is2(node.A_Expr.rexpr, "ColumnRef")) {
|
|
383
|
+
this.add(node.A_Expr.rexpr, { jsonbOperator });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
346
387
|
if (is2(node, "ColumnRef")) {
|
|
347
388
|
for (let i = 0; i < stack.length; i++) {
|
|
348
389
|
const inReturningList = stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef";
|
|
@@ -441,9 +482,15 @@ var Walker = class _Walker {
|
|
|
441
482
|
if (options?.where) {
|
|
442
483
|
ref.where = options.where;
|
|
443
484
|
}
|
|
485
|
+
if (options?.jsonbOperator) {
|
|
486
|
+
ref.jsonbOperator = options.jsonbOperator;
|
|
487
|
+
}
|
|
444
488
|
this.highlights.push(ref);
|
|
445
489
|
}
|
|
446
|
-
static traverse(node,
|
|
490
|
+
static traverse(node, callback) {
|
|
491
|
+
_Walker.doTraverse(node, [], callback);
|
|
492
|
+
}
|
|
493
|
+
static doTraverse(node, stack, callback) {
|
|
447
494
|
if (isANode2(node)) {
|
|
448
495
|
callback(node, [...stack, getNodeKind(node)]);
|
|
449
496
|
}
|
|
@@ -453,15 +500,19 @@ var Walker = class _Walker {
|
|
|
453
500
|
if (Array.isArray(node)) {
|
|
454
501
|
for (const item of node) {
|
|
455
502
|
if (isANode2(item)) {
|
|
456
|
-
_Walker.
|
|
503
|
+
_Walker.doTraverse(item, stack, callback);
|
|
457
504
|
}
|
|
458
505
|
}
|
|
459
506
|
} else if (isANode2(node)) {
|
|
460
507
|
const keys = Object.keys(node);
|
|
461
|
-
_Walker.
|
|
508
|
+
_Walker.doTraverse(node[keys[0]], [...stack, getNodeKind(node)], callback);
|
|
462
509
|
} else {
|
|
463
510
|
for (const [key, child] of Object.entries(node)) {
|
|
464
|
-
_Walker.
|
|
511
|
+
_Walker.doTraverse(
|
|
512
|
+
child,
|
|
513
|
+
[...stack, key],
|
|
514
|
+
callback
|
|
515
|
+
);
|
|
465
516
|
}
|
|
466
517
|
}
|
|
467
518
|
}
|
|
@@ -605,6 +656,9 @@ var Analyzer = class {
|
|
|
605
656
|
if (colReference.where) {
|
|
606
657
|
index.where = colReference.where;
|
|
607
658
|
}
|
|
659
|
+
if (colReference.jsonbOperator) {
|
|
660
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
661
|
+
}
|
|
608
662
|
addIndex(index);
|
|
609
663
|
}
|
|
610
664
|
} else if (tableReference) {
|
|
@@ -626,6 +680,9 @@ var Analyzer = class {
|
|
|
626
680
|
if (colReference.where) {
|
|
627
681
|
index.where = colReference.where;
|
|
628
682
|
}
|
|
683
|
+
if (colReference.jsonbOperator) {
|
|
684
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
685
|
+
}
|
|
629
686
|
addIndex(index);
|
|
630
687
|
}
|
|
631
688
|
} else if (fullReference) {
|
|
@@ -644,6 +701,9 @@ var Analyzer = class {
|
|
|
644
701
|
if (colReference.where) {
|
|
645
702
|
index.where = colReference.where;
|
|
646
703
|
}
|
|
704
|
+
if (colReference.jsonbOperator) {
|
|
705
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
706
|
+
}
|
|
647
707
|
addIndex(index);
|
|
648
708
|
} else {
|
|
649
709
|
console.error(
|
|
@@ -766,7 +826,7 @@ async function dropIndex(tx, index) {
|
|
|
766
826
|
|
|
767
827
|
// src/sql/indexes.ts
|
|
768
828
|
function isIndexSupported(index) {
|
|
769
|
-
return index.index_type === "btree";
|
|
829
|
+
return index.index_type === "btree" || index.index_type === "gin";
|
|
770
830
|
}
|
|
771
831
|
function isIndexProbablyDroppable(index) {
|
|
772
832
|
return !index.is_primary && !index.is_unique;
|
|
@@ -1248,8 +1308,10 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1248
1308
|
* Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
|
|
1249
1309
|
**/
|
|
1250
1310
|
indexesToCreate(rootCandidates) {
|
|
1251
|
-
const
|
|
1311
|
+
const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
|
|
1312
|
+
const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
|
|
1252
1313
|
const nextStage = [];
|
|
1314
|
+
const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
|
|
1253
1315
|
for (const permutation of permutedIndexes.values()) {
|
|
1254
1316
|
const { table: rawTable, schema: rawSchema, columns } = permutation;
|
|
1255
1317
|
const permutations = permutationsWithDescendingLength(columns);
|
|
@@ -1274,6 +1336,41 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1274
1336
|
});
|
|
1275
1337
|
}
|
|
1276
1338
|
}
|
|
1339
|
+
const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
|
|
1340
|
+
for (const group of ginGroups.values()) {
|
|
1341
|
+
const { schema: rawSchema, table: rawTable, column, operators } = group;
|
|
1342
|
+
const schema = PgIdentifier.fromString(rawSchema);
|
|
1343
|
+
const table = PgIdentifier.fromString(rawTable);
|
|
1344
|
+
const needsKeyExistence = operators.some(
|
|
1345
|
+
(op) => op === "?" || op === "?|" || op === "?&"
|
|
1346
|
+
);
|
|
1347
|
+
const opclass = needsKeyExistence ? void 0 : "jsonb_path_ops";
|
|
1348
|
+
const existingGin = this.ginIndexAlreadyExists(table.toString(), column);
|
|
1349
|
+
if (existingGin) {
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
const indexName = this.indexName();
|
|
1353
|
+
const candidate = {
|
|
1354
|
+
schema: rawSchema,
|
|
1355
|
+
table: rawTable,
|
|
1356
|
+
column
|
|
1357
|
+
};
|
|
1358
|
+
const definition = this.toGinDefinition({
|
|
1359
|
+
table,
|
|
1360
|
+
schema,
|
|
1361
|
+
column: PgIdentifier.fromString(column),
|
|
1362
|
+
opclass
|
|
1363
|
+
});
|
|
1364
|
+
nextStage.push({
|
|
1365
|
+
name: indexName,
|
|
1366
|
+
schema: schema.toString(),
|
|
1367
|
+
table: table.toString(),
|
|
1368
|
+
columns: [candidate],
|
|
1369
|
+
definition,
|
|
1370
|
+
indexMethod: "gin",
|
|
1371
|
+
opclass
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1277
1374
|
return nextStage;
|
|
1278
1375
|
}
|
|
1279
1376
|
toDefinition({
|
|
@@ -1308,6 +1405,47 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1308
1405
|
const colored = make(green, yellow, magenta, blue2);
|
|
1309
1406
|
return { raw, colored };
|
|
1310
1407
|
}
|
|
1408
|
+
toGinDefinition({
|
|
1409
|
+
schema,
|
|
1410
|
+
table,
|
|
1411
|
+
column,
|
|
1412
|
+
opclass
|
|
1413
|
+
}) {
|
|
1414
|
+
let fullyQualifiedTable;
|
|
1415
|
+
if (schema.toString() === "public") {
|
|
1416
|
+
fullyQualifiedTable = table;
|
|
1417
|
+
} else {
|
|
1418
|
+
fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
|
|
1419
|
+
}
|
|
1420
|
+
const opclassSuffix = opclass ? ` ${opclass}` : "";
|
|
1421
|
+
return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
|
|
1422
|
+
}
|
|
1423
|
+
groupGinCandidatesByColumn(candidates) {
|
|
1424
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1425
|
+
for (const c of candidates) {
|
|
1426
|
+
if (!c.jsonbOperator) continue;
|
|
1427
|
+
const key = `${c.schema}.${c.table}.${c.column}`;
|
|
1428
|
+
const existing = groups.get(key);
|
|
1429
|
+
if (existing) {
|
|
1430
|
+
if (!existing.operators.includes(c.jsonbOperator)) {
|
|
1431
|
+
existing.operators.push(c.jsonbOperator);
|
|
1432
|
+
}
|
|
1433
|
+
} else {
|
|
1434
|
+
groups.set(key, {
|
|
1435
|
+
schema: c.schema,
|
|
1436
|
+
table: c.table,
|
|
1437
|
+
column: c.column,
|
|
1438
|
+
operators: [c.jsonbOperator]
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return groups;
|
|
1443
|
+
}
|
|
1444
|
+
ginIndexAlreadyExists(table, column) {
|
|
1445
|
+
return this.existingIndexes.find(
|
|
1446
|
+
(index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column)
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1311
1449
|
/**
|
|
1312
1450
|
* Drop indexes that can be dropped. Ignore the ones that can't
|
|
1313
1451
|
*/
|
|
@@ -1624,6 +1762,21 @@ var _Statistics = class _Statistics {
|
|
|
1624
1762
|
}
|
|
1625
1763
|
return "text";
|
|
1626
1764
|
}
|
|
1765
|
+
/**
|
|
1766
|
+
* PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays
|
|
1767
|
+
* for columns with array types (e.g. text[], int4[]). These create
|
|
1768
|
+
* multidimensional arrays that can be "ragged" (sub-arrays with different
|
|
1769
|
+
* lengths). jsonb_to_recordset can't reconstruct ragged multidimensional
|
|
1770
|
+
* arrays from JSON, so we need to drop these values.
|
|
1771
|
+
*/
|
|
1772
|
+
static safeStavalues(values) {
|
|
1773
|
+
if (!values || values.length === 0) return values;
|
|
1774
|
+
if (values.some((v) => Array.isArray(v))) {
|
|
1775
|
+
console.warn("Discarding ragged multidimensional stavalues array");
|
|
1776
|
+
return null;
|
|
1777
|
+
}
|
|
1778
|
+
return values;
|
|
1779
|
+
}
|
|
1627
1780
|
async restoreStats17(tx) {
|
|
1628
1781
|
const warnings = {
|
|
1629
1782
|
tablesNotInExports: [],
|
|
@@ -1675,16 +1828,26 @@ var _Statistics = class _Statistics {
|
|
|
1675
1828
|
stanumbers3: stats.stanumbers3,
|
|
1676
1829
|
stanumbers4: stats.stanumbers4,
|
|
1677
1830
|
stanumbers5: stats.stanumbers5,
|
|
1678
|
-
stavalues1: stats.stavalues1,
|
|
1679
|
-
stavalues2: stats.stavalues2,
|
|
1680
|
-
stavalues3: stats.stavalues3,
|
|
1681
|
-
stavalues4: stats.stavalues4,
|
|
1682
|
-
stavalues5: stats.stavalues5,
|
|
1683
|
-
_value_type1: this.stavalueKind(
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1831
|
+
stavalues1: _Statistics.safeStavalues(stats.stavalues1),
|
|
1832
|
+
stavalues2: _Statistics.safeStavalues(stats.stavalues2),
|
|
1833
|
+
stavalues3: _Statistics.safeStavalues(stats.stavalues3),
|
|
1834
|
+
stavalues4: _Statistics.safeStavalues(stats.stavalues4),
|
|
1835
|
+
stavalues5: _Statistics.safeStavalues(stats.stavalues5),
|
|
1836
|
+
_value_type1: this.stavalueKind(
|
|
1837
|
+
_Statistics.safeStavalues(stats.stavalues1)
|
|
1838
|
+
),
|
|
1839
|
+
_value_type2: this.stavalueKind(
|
|
1840
|
+
_Statistics.safeStavalues(stats.stavalues2)
|
|
1841
|
+
),
|
|
1842
|
+
_value_type3: this.stavalueKind(
|
|
1843
|
+
_Statistics.safeStavalues(stats.stavalues3)
|
|
1844
|
+
),
|
|
1845
|
+
_value_type4: this.stavalueKind(
|
|
1846
|
+
_Statistics.safeStavalues(stats.stavalues4)
|
|
1847
|
+
),
|
|
1848
|
+
_value_type5: this.stavalueKind(
|
|
1849
|
+
_Statistics.safeStavalues(stats.stavalues5)
|
|
1850
|
+
)
|
|
1688
1851
|
});
|
|
1689
1852
|
}
|
|
1690
1853
|
}
|
|
@@ -2100,14 +2263,16 @@ var _Statistics = class _Statistics {
|
|
|
2100
2263
|
CASE
|
|
2101
2264
|
WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
|
|
2102
2265
|
ELSE 'ASC'
|
|
2103
|
-
END
|
|
2266
|
+
END,
|
|
2267
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2104
2268
|
-- Handle expressions
|
|
2105
2269
|
ELSE
|
|
2106
2270
|
json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
|
|
2107
2271
|
CASE
|
|
2108
2272
|
WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
|
|
2109
2273
|
ELSE 'ASC'
|
|
2110
|
-
END
|
|
2274
|
+
END,
|
|
2275
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2111
2276
|
END
|
|
2112
2277
|
ORDER BY array_position(ix.indkey, k.attnum)
|
|
2113
2278
|
) AS index_columns
|
|
@@ -2119,6 +2284,7 @@ var _Statistics = class _Statistics {
|
|
|
2119
2284
|
JOIN pg_am am ON i.relam = am.oid
|
|
2120
2285
|
LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
|
|
2121
2286
|
LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
|
|
2287
|
+
LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]
|
|
2122
2288
|
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
2123
2289
|
WHERE
|
|
2124
2290
|
n.nspname not like 'pg_%' and
|