@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.
Files changed (45) 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 +202 -36
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.js +202 -36
  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.d.ts +7 -0
  14. package/dist/optimizer/genalgo.d.ts.map +1 -1
  15. package/dist/optimizer/genalgo.test.d.ts +2 -0
  16. package/dist/optimizer/genalgo.test.d.ts.map +1 -0
  17. package/dist/optimizer/index-candidate.d.ts +23 -0
  18. package/dist/optimizer/index-candidate.d.ts.map +1 -0
  19. package/dist/optimizer/index-shrinker.d.ts +10 -0
  20. package/dist/optimizer/index-shrinker.d.ts.map +1 -0
  21. package/dist/optimizer/index-shrinker.test.d.ts +2 -0
  22. package/dist/optimizer/index-shrinker.test.d.ts.map +1 -0
  23. package/dist/optimizer/index-tester.d.ts +2 -0
  24. package/dist/optimizer/index-tester.d.ts.map +1 -0
  25. package/dist/optimizer/pss-rewriter.test.d.ts +2 -0
  26. package/dist/optimizer/pss-rewriter.test.d.ts.map +1 -0
  27. package/dist/optimizer/statistics.d.ts +9 -0
  28. package/dist/optimizer/statistics.d.ts.map +1 -1
  29. package/dist/sql/analyzer.d.ts +2 -0
  30. package/dist/sql/analyzer.d.ts.map +1 -1
  31. package/dist/sql/analyzer.test.d.ts +2 -0
  32. package/dist/sql/analyzer.test.d.ts.map +1 -0
  33. package/dist/sql/builder.test.d.ts +2 -0
  34. package/dist/sql/builder.test.d.ts.map +1 -0
  35. package/dist/sql/nudges.d.ts +1 -0
  36. package/dist/sql/nudges.d.ts.map +1 -1
  37. package/dist/sql/permutations.test.d.ts +2 -0
  38. package/dist/sql/permutations.test.d.ts.map +1 -0
  39. package/dist/sql/pg-identifier.test.d.ts +2 -0
  40. package/dist/sql/pg-identifier.test.d.ts.map +1 -0
  41. package/dist/sql/walker.d.ts +2 -1
  42. package/dist/sql/walker.d.ts.map +1 -1
  43. package/dist/sql/walker.test.d.ts +2 -0
  44. package/dist/sql/walker.test.d.ts.map +1 -0
  45. 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")) {
@@ -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, stack, callback) {
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.traverse(item, stack, callback);
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.traverse(node[keys[0]], [...stack, getNodeKind(node)], callback);
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.traverse(child, [...stack, key], callback);
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 permutedIndexes = this.groupPotentialIndexColumnsByTable(rootCandidates);
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(stats.stavalues1),
1733
- _value_type2: this.stavalueKind(stats.stavalues2),
1734
- _value_type3: this.stavalueKind(stats.stavalues3),
1735
- _value_type4: this.stavalueKind(stats.stavalues4),
1736
- _value_type5: this.stavalueKind(stats.stavalues5)
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