@query-doctor/core 0.3.0 → 0.4.0
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/index.cjs +144 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +144 -14
- package/dist/index.js.map +1 -1
- package/dist/optimizer/genalgo.d.ts +7 -0
- package/dist/optimizer/genalgo.d.ts.map +1 -1
- 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/walker.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -343,6 +343,18 @@ var Walker = class _Walker {
|
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
|
+
if (is2(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
|
|
347
|
+
const opName = node.A_Expr.name?.[0] && is2(node.A_Expr.name[0], "String") && node.A_Expr.name[0].String.sval;
|
|
348
|
+
if (opName && (opName === "@>" || opName === "?" || opName === "?|" || opName === "?&")) {
|
|
349
|
+
const jsonbOperator = opName;
|
|
350
|
+
if (node.A_Expr.lexpr && is2(node.A_Expr.lexpr, "ColumnRef")) {
|
|
351
|
+
this.add(node.A_Expr.lexpr, { jsonbOperator });
|
|
352
|
+
}
|
|
353
|
+
if (node.A_Expr.rexpr && is2(node.A_Expr.rexpr, "ColumnRef")) {
|
|
354
|
+
this.add(node.A_Expr.rexpr, { jsonbOperator });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
346
358
|
if (is2(node, "ColumnRef")) {
|
|
347
359
|
for (let i = 0; i < stack.length; i++) {
|
|
348
360
|
const inReturningList = stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef";
|
|
@@ -441,6 +453,9 @@ var Walker = class _Walker {
|
|
|
441
453
|
if (options?.where) {
|
|
442
454
|
ref.where = options.where;
|
|
443
455
|
}
|
|
456
|
+
if (options?.jsonbOperator) {
|
|
457
|
+
ref.jsonbOperator = options.jsonbOperator;
|
|
458
|
+
}
|
|
444
459
|
this.highlights.push(ref);
|
|
445
460
|
}
|
|
446
461
|
static traverse(node, stack, callback) {
|
|
@@ -605,6 +620,9 @@ var Analyzer = class {
|
|
|
605
620
|
if (colReference.where) {
|
|
606
621
|
index.where = colReference.where;
|
|
607
622
|
}
|
|
623
|
+
if (colReference.jsonbOperator) {
|
|
624
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
625
|
+
}
|
|
608
626
|
addIndex(index);
|
|
609
627
|
}
|
|
610
628
|
} else if (tableReference) {
|
|
@@ -626,6 +644,9 @@ var Analyzer = class {
|
|
|
626
644
|
if (colReference.where) {
|
|
627
645
|
index.where = colReference.where;
|
|
628
646
|
}
|
|
647
|
+
if (colReference.jsonbOperator) {
|
|
648
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
649
|
+
}
|
|
629
650
|
addIndex(index);
|
|
630
651
|
}
|
|
631
652
|
} else if (fullReference) {
|
|
@@ -644,6 +665,9 @@ var Analyzer = class {
|
|
|
644
665
|
if (colReference.where) {
|
|
645
666
|
index.where = colReference.where;
|
|
646
667
|
}
|
|
668
|
+
if (colReference.jsonbOperator) {
|
|
669
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
670
|
+
}
|
|
647
671
|
addIndex(index);
|
|
648
672
|
} else {
|
|
649
673
|
console.error(
|
|
@@ -766,7 +790,7 @@ async function dropIndex(tx, index) {
|
|
|
766
790
|
|
|
767
791
|
// src/sql/indexes.ts
|
|
768
792
|
function isIndexSupported(index) {
|
|
769
|
-
return index.index_type === "btree";
|
|
793
|
+
return index.index_type === "btree" || index.index_type === "gin";
|
|
770
794
|
}
|
|
771
795
|
function isIndexProbablyDroppable(index) {
|
|
772
796
|
return !index.is_primary && !index.is_unique;
|
|
@@ -1248,8 +1272,10 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1248
1272
|
* Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
|
|
1249
1273
|
**/
|
|
1250
1274
|
indexesToCreate(rootCandidates) {
|
|
1251
|
-
const
|
|
1275
|
+
const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
|
|
1276
|
+
const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
|
|
1252
1277
|
const nextStage = [];
|
|
1278
|
+
const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
|
|
1253
1279
|
for (const permutation of permutedIndexes.values()) {
|
|
1254
1280
|
const { table: rawTable, schema: rawSchema, columns } = permutation;
|
|
1255
1281
|
const permutations = permutationsWithDescendingLength(columns);
|
|
@@ -1274,6 +1300,41 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1274
1300
|
});
|
|
1275
1301
|
}
|
|
1276
1302
|
}
|
|
1303
|
+
const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
|
|
1304
|
+
for (const group of ginGroups.values()) {
|
|
1305
|
+
const { schema: rawSchema, table: rawTable, column, operators } = group;
|
|
1306
|
+
const schema = PgIdentifier.fromString(rawSchema);
|
|
1307
|
+
const table = PgIdentifier.fromString(rawTable);
|
|
1308
|
+
const needsKeyExistence = operators.some(
|
|
1309
|
+
(op) => op === "?" || op === "?|" || op === "?&"
|
|
1310
|
+
);
|
|
1311
|
+
const opclass = needsKeyExistence ? void 0 : "jsonb_path_ops";
|
|
1312
|
+
const existingGin = this.ginIndexAlreadyExists(table.toString(), column);
|
|
1313
|
+
if (existingGin) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
const indexName = this.indexName();
|
|
1317
|
+
const candidate = {
|
|
1318
|
+
schema: rawSchema,
|
|
1319
|
+
table: rawTable,
|
|
1320
|
+
column
|
|
1321
|
+
};
|
|
1322
|
+
const definition = this.toGinDefinition({
|
|
1323
|
+
table,
|
|
1324
|
+
schema,
|
|
1325
|
+
column: PgIdentifier.fromString(column),
|
|
1326
|
+
opclass
|
|
1327
|
+
});
|
|
1328
|
+
nextStage.push({
|
|
1329
|
+
name: indexName,
|
|
1330
|
+
schema: schema.toString(),
|
|
1331
|
+
table: table.toString(),
|
|
1332
|
+
columns: [candidate],
|
|
1333
|
+
definition,
|
|
1334
|
+
indexMethod: "gin",
|
|
1335
|
+
opclass
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1277
1338
|
return nextStage;
|
|
1278
1339
|
}
|
|
1279
1340
|
toDefinition({
|
|
@@ -1308,6 +1369,47 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1308
1369
|
const colored = make(green, yellow, magenta, blue2);
|
|
1309
1370
|
return { raw, colored };
|
|
1310
1371
|
}
|
|
1372
|
+
toGinDefinition({
|
|
1373
|
+
schema,
|
|
1374
|
+
table,
|
|
1375
|
+
column,
|
|
1376
|
+
opclass
|
|
1377
|
+
}) {
|
|
1378
|
+
let fullyQualifiedTable;
|
|
1379
|
+
if (schema.toString() === "public") {
|
|
1380
|
+
fullyQualifiedTable = table;
|
|
1381
|
+
} else {
|
|
1382
|
+
fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
|
|
1383
|
+
}
|
|
1384
|
+
const opclassSuffix = opclass ? ` ${opclass}` : "";
|
|
1385
|
+
return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
|
|
1386
|
+
}
|
|
1387
|
+
groupGinCandidatesByColumn(candidates) {
|
|
1388
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1389
|
+
for (const c of candidates) {
|
|
1390
|
+
if (!c.jsonbOperator) continue;
|
|
1391
|
+
const key = `${c.schema}.${c.table}.${c.column}`;
|
|
1392
|
+
const existing = groups.get(key);
|
|
1393
|
+
if (existing) {
|
|
1394
|
+
if (!existing.operators.includes(c.jsonbOperator)) {
|
|
1395
|
+
existing.operators.push(c.jsonbOperator);
|
|
1396
|
+
}
|
|
1397
|
+
} else {
|
|
1398
|
+
groups.set(key, {
|
|
1399
|
+
schema: c.schema,
|
|
1400
|
+
table: c.table,
|
|
1401
|
+
column: c.column,
|
|
1402
|
+
operators: [c.jsonbOperator]
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return groups;
|
|
1407
|
+
}
|
|
1408
|
+
ginIndexAlreadyExists(table, column) {
|
|
1409
|
+
return this.existingIndexes.find(
|
|
1410
|
+
(index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column)
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1311
1413
|
/**
|
|
1312
1414
|
* Drop indexes that can be dropped. Ignore the ones that can't
|
|
1313
1415
|
*/
|
|
@@ -1624,6 +1726,21 @@ var _Statistics = class _Statistics {
|
|
|
1624
1726
|
}
|
|
1625
1727
|
return "text";
|
|
1626
1728
|
}
|
|
1729
|
+
/**
|
|
1730
|
+
* PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays
|
|
1731
|
+
* for columns with array types (e.g. text[], int4[]). These create
|
|
1732
|
+
* multidimensional arrays that can be "ragged" (sub-arrays with different
|
|
1733
|
+
* lengths). jsonb_to_recordset can't reconstruct ragged multidimensional
|
|
1734
|
+
* arrays from JSON, so we need to drop these values.
|
|
1735
|
+
*/
|
|
1736
|
+
static safeStavalues(values) {
|
|
1737
|
+
if (!values || values.length === 0) return values;
|
|
1738
|
+
if (values.some((v) => Array.isArray(v))) {
|
|
1739
|
+
console.warn("Discarding ragged multidimensional stavalues array");
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
return values;
|
|
1743
|
+
}
|
|
1627
1744
|
async restoreStats17(tx) {
|
|
1628
1745
|
const warnings = {
|
|
1629
1746
|
tablesNotInExports: [],
|
|
@@ -1675,16 +1792,26 @@ var _Statistics = class _Statistics {
|
|
|
1675
1792
|
stanumbers3: stats.stanumbers3,
|
|
1676
1793
|
stanumbers4: stats.stanumbers4,
|
|
1677
1794
|
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
|
-
|
|
1795
|
+
stavalues1: _Statistics.safeStavalues(stats.stavalues1),
|
|
1796
|
+
stavalues2: _Statistics.safeStavalues(stats.stavalues2),
|
|
1797
|
+
stavalues3: _Statistics.safeStavalues(stats.stavalues3),
|
|
1798
|
+
stavalues4: _Statistics.safeStavalues(stats.stavalues4),
|
|
1799
|
+
stavalues5: _Statistics.safeStavalues(stats.stavalues5),
|
|
1800
|
+
_value_type1: this.stavalueKind(
|
|
1801
|
+
_Statistics.safeStavalues(stats.stavalues1)
|
|
1802
|
+
),
|
|
1803
|
+
_value_type2: this.stavalueKind(
|
|
1804
|
+
_Statistics.safeStavalues(stats.stavalues2)
|
|
1805
|
+
),
|
|
1806
|
+
_value_type3: this.stavalueKind(
|
|
1807
|
+
_Statistics.safeStavalues(stats.stavalues3)
|
|
1808
|
+
),
|
|
1809
|
+
_value_type4: this.stavalueKind(
|
|
1810
|
+
_Statistics.safeStavalues(stats.stavalues4)
|
|
1811
|
+
),
|
|
1812
|
+
_value_type5: this.stavalueKind(
|
|
1813
|
+
_Statistics.safeStavalues(stats.stavalues5)
|
|
1814
|
+
)
|
|
1688
1815
|
});
|
|
1689
1816
|
}
|
|
1690
1817
|
}
|
|
@@ -2100,14 +2227,16 @@ var _Statistics = class _Statistics {
|
|
|
2100
2227
|
CASE
|
|
2101
2228
|
WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
|
|
2102
2229
|
ELSE 'ASC'
|
|
2103
|
-
END
|
|
2230
|
+
END,
|
|
2231
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2104
2232
|
-- Handle expressions
|
|
2105
2233
|
ELSE
|
|
2106
2234
|
json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
|
|
2107
2235
|
CASE
|
|
2108
2236
|
WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
|
|
2109
2237
|
ELSE 'ASC'
|
|
2110
|
-
END
|
|
2238
|
+
END,
|
|
2239
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2111
2240
|
END
|
|
2112
2241
|
ORDER BY array_position(ix.indkey, k.attnum)
|
|
2113
2242
|
) AS index_columns
|
|
@@ -2119,6 +2248,7 @@ var _Statistics = class _Statistics {
|
|
|
2119
2248
|
JOIN pg_am am ON i.relam = am.oid
|
|
2120
2249
|
LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
|
|
2121
2250
|
LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
|
|
2251
|
+
LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]
|
|
2122
2252
|
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
2123
2253
|
WHERE
|
|
2124
2254
|
n.nspname not like 'pg_%' and
|