@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.cjs
CHANGED
|
@@ -392,6 +392,18 @@ var Walker = class _Walker {
|
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
|
+
if (is2(node, "A_Expr") && node.A_Expr.kind === "AEXPR_OP") {
|
|
396
|
+
const opName = node.A_Expr.name?.[0] && is2(node.A_Expr.name[0], "String") && node.A_Expr.name[0].String.sval;
|
|
397
|
+
if (opName && (opName === "@>" || opName === "?" || opName === "?|" || opName === "?&")) {
|
|
398
|
+
const jsonbOperator = opName;
|
|
399
|
+
if (node.A_Expr.lexpr && is2(node.A_Expr.lexpr, "ColumnRef")) {
|
|
400
|
+
this.add(node.A_Expr.lexpr, { jsonbOperator });
|
|
401
|
+
}
|
|
402
|
+
if (node.A_Expr.rexpr && is2(node.A_Expr.rexpr, "ColumnRef")) {
|
|
403
|
+
this.add(node.A_Expr.rexpr, { jsonbOperator });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
395
407
|
if (is2(node, "ColumnRef")) {
|
|
396
408
|
for (let i = 0; i < stack.length; i++) {
|
|
397
409
|
const inReturningList = stack[i] === "returningList" && stack[i + 1] === "ResTarget" && stack[i + 2] === "val" && stack[i + 3] === "ColumnRef";
|
|
@@ -490,6 +502,9 @@ var Walker = class _Walker {
|
|
|
490
502
|
if (options?.where) {
|
|
491
503
|
ref.where = options.where;
|
|
492
504
|
}
|
|
505
|
+
if (options?.jsonbOperator) {
|
|
506
|
+
ref.jsonbOperator = options.jsonbOperator;
|
|
507
|
+
}
|
|
493
508
|
this.highlights.push(ref);
|
|
494
509
|
}
|
|
495
510
|
static traverse(node, stack, callback) {
|
|
@@ -654,6 +669,9 @@ var Analyzer = class {
|
|
|
654
669
|
if (colReference.where) {
|
|
655
670
|
index.where = colReference.where;
|
|
656
671
|
}
|
|
672
|
+
if (colReference.jsonbOperator) {
|
|
673
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
674
|
+
}
|
|
657
675
|
addIndex(index);
|
|
658
676
|
}
|
|
659
677
|
} else if (tableReference) {
|
|
@@ -675,6 +693,9 @@ var Analyzer = class {
|
|
|
675
693
|
if (colReference.where) {
|
|
676
694
|
index.where = colReference.where;
|
|
677
695
|
}
|
|
696
|
+
if (colReference.jsonbOperator) {
|
|
697
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
698
|
+
}
|
|
678
699
|
addIndex(index);
|
|
679
700
|
}
|
|
680
701
|
} else if (fullReference) {
|
|
@@ -693,6 +714,9 @@ var Analyzer = class {
|
|
|
693
714
|
if (colReference.where) {
|
|
694
715
|
index.where = colReference.where;
|
|
695
716
|
}
|
|
717
|
+
if (colReference.jsonbOperator) {
|
|
718
|
+
index.jsonbOperator = colReference.jsonbOperator;
|
|
719
|
+
}
|
|
696
720
|
addIndex(index);
|
|
697
721
|
} else {
|
|
698
722
|
console.error(
|
|
@@ -815,7 +839,7 @@ async function dropIndex(tx, index) {
|
|
|
815
839
|
|
|
816
840
|
// src/sql/indexes.ts
|
|
817
841
|
function isIndexSupported(index) {
|
|
818
|
-
return index.index_type === "btree";
|
|
842
|
+
return index.index_type === "btree" || index.index_type === "gin";
|
|
819
843
|
}
|
|
820
844
|
function isIndexProbablyDroppable(index) {
|
|
821
845
|
return !index.is_primary && !index.is_unique;
|
|
@@ -1297,8 +1321,10 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1297
1321
|
* Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
|
|
1298
1322
|
**/
|
|
1299
1323
|
indexesToCreate(rootCandidates) {
|
|
1300
|
-
const
|
|
1324
|
+
const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
|
|
1325
|
+
const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
|
|
1301
1326
|
const nextStage = [];
|
|
1327
|
+
const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
|
|
1302
1328
|
for (const permutation of permutedIndexes.values()) {
|
|
1303
1329
|
const { table: rawTable, schema: rawSchema, columns } = permutation;
|
|
1304
1330
|
const permutations = permutationsWithDescendingLength(columns);
|
|
@@ -1323,6 +1349,41 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1323
1349
|
});
|
|
1324
1350
|
}
|
|
1325
1351
|
}
|
|
1352
|
+
const ginGroups = this.groupGinCandidatesByColumn(ginCandidates);
|
|
1353
|
+
for (const group of ginGroups.values()) {
|
|
1354
|
+
const { schema: rawSchema, table: rawTable, column, operators } = group;
|
|
1355
|
+
const schema = PgIdentifier.fromString(rawSchema);
|
|
1356
|
+
const table = PgIdentifier.fromString(rawTable);
|
|
1357
|
+
const needsKeyExistence = operators.some(
|
|
1358
|
+
(op) => op === "?" || op === "?|" || op === "?&"
|
|
1359
|
+
);
|
|
1360
|
+
const opclass = needsKeyExistence ? void 0 : "jsonb_path_ops";
|
|
1361
|
+
const existingGin = this.ginIndexAlreadyExists(table.toString(), column);
|
|
1362
|
+
if (existingGin) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const indexName = this.indexName();
|
|
1366
|
+
const candidate = {
|
|
1367
|
+
schema: rawSchema,
|
|
1368
|
+
table: rawTable,
|
|
1369
|
+
column
|
|
1370
|
+
};
|
|
1371
|
+
const definition = this.toGinDefinition({
|
|
1372
|
+
table,
|
|
1373
|
+
schema,
|
|
1374
|
+
column: PgIdentifier.fromString(column),
|
|
1375
|
+
opclass
|
|
1376
|
+
});
|
|
1377
|
+
nextStage.push({
|
|
1378
|
+
name: indexName,
|
|
1379
|
+
schema: schema.toString(),
|
|
1380
|
+
table: table.toString(),
|
|
1381
|
+
columns: [candidate],
|
|
1382
|
+
definition,
|
|
1383
|
+
indexMethod: "gin",
|
|
1384
|
+
opclass
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1326
1387
|
return nextStage;
|
|
1327
1388
|
}
|
|
1328
1389
|
toDefinition({
|
|
@@ -1357,6 +1418,47 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1357
1418
|
const colored = make(import_colorette2.green, import_colorette2.yellow, import_colorette2.magenta, import_colorette2.blue);
|
|
1358
1419
|
return { raw, colored };
|
|
1359
1420
|
}
|
|
1421
|
+
toGinDefinition({
|
|
1422
|
+
schema,
|
|
1423
|
+
table,
|
|
1424
|
+
column,
|
|
1425
|
+
opclass
|
|
1426
|
+
}) {
|
|
1427
|
+
let fullyQualifiedTable;
|
|
1428
|
+
if (schema.toString() === "public") {
|
|
1429
|
+
fullyQualifiedTable = table;
|
|
1430
|
+
} else {
|
|
1431
|
+
fullyQualifiedTable = PgIdentifier.fromParts(schema, table);
|
|
1432
|
+
}
|
|
1433
|
+
const opclassSuffix = opclass ? ` ${opclass}` : "";
|
|
1434
|
+
return `${fullyQualifiedTable} using gin (${column}${opclassSuffix})`;
|
|
1435
|
+
}
|
|
1436
|
+
groupGinCandidatesByColumn(candidates) {
|
|
1437
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1438
|
+
for (const c of candidates) {
|
|
1439
|
+
if (!c.jsonbOperator) continue;
|
|
1440
|
+
const key = `${c.schema}.${c.table}.${c.column}`;
|
|
1441
|
+
const existing = groups.get(key);
|
|
1442
|
+
if (existing) {
|
|
1443
|
+
if (!existing.operators.includes(c.jsonbOperator)) {
|
|
1444
|
+
existing.operators.push(c.jsonbOperator);
|
|
1445
|
+
}
|
|
1446
|
+
} else {
|
|
1447
|
+
groups.set(key, {
|
|
1448
|
+
schema: c.schema,
|
|
1449
|
+
table: c.table,
|
|
1450
|
+
column: c.column,
|
|
1451
|
+
operators: [c.jsonbOperator]
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return groups;
|
|
1456
|
+
}
|
|
1457
|
+
ginIndexAlreadyExists(table, column) {
|
|
1458
|
+
return this.existingIndexes.find(
|
|
1459
|
+
(index) => index.index_type === "gin" && index.table_name === table && index.index_columns.some((c) => c.name === column)
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1360
1462
|
/**
|
|
1361
1463
|
* Drop indexes that can be dropped. Ignore the ones that can't
|
|
1362
1464
|
*/
|
|
@@ -1673,6 +1775,21 @@ var _Statistics = class _Statistics {
|
|
|
1673
1775
|
}
|
|
1674
1776
|
return "text";
|
|
1675
1777
|
}
|
|
1778
|
+
/**
|
|
1779
|
+
* PostgreSQL's anyarray columns in pg_statistic can hold arrays of arrays
|
|
1780
|
+
* for columns with array types (e.g. text[], int4[]). These create
|
|
1781
|
+
* multidimensional arrays that can be "ragged" (sub-arrays with different
|
|
1782
|
+
* lengths). jsonb_to_recordset can't reconstruct ragged multidimensional
|
|
1783
|
+
* arrays from JSON, so we need to drop these values.
|
|
1784
|
+
*/
|
|
1785
|
+
static safeStavalues(values) {
|
|
1786
|
+
if (!values || values.length === 0) return values;
|
|
1787
|
+
if (values.some((v) => Array.isArray(v))) {
|
|
1788
|
+
console.warn("Discarding ragged multidimensional stavalues array");
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
return values;
|
|
1792
|
+
}
|
|
1676
1793
|
async restoreStats17(tx) {
|
|
1677
1794
|
const warnings = {
|
|
1678
1795
|
tablesNotInExports: [],
|
|
@@ -1724,16 +1841,26 @@ var _Statistics = class _Statistics {
|
|
|
1724
1841
|
stanumbers3: stats.stanumbers3,
|
|
1725
1842
|
stanumbers4: stats.stanumbers4,
|
|
1726
1843
|
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
|
-
|
|
1844
|
+
stavalues1: _Statistics.safeStavalues(stats.stavalues1),
|
|
1845
|
+
stavalues2: _Statistics.safeStavalues(stats.stavalues2),
|
|
1846
|
+
stavalues3: _Statistics.safeStavalues(stats.stavalues3),
|
|
1847
|
+
stavalues4: _Statistics.safeStavalues(stats.stavalues4),
|
|
1848
|
+
stavalues5: _Statistics.safeStavalues(stats.stavalues5),
|
|
1849
|
+
_value_type1: this.stavalueKind(
|
|
1850
|
+
_Statistics.safeStavalues(stats.stavalues1)
|
|
1851
|
+
),
|
|
1852
|
+
_value_type2: this.stavalueKind(
|
|
1853
|
+
_Statistics.safeStavalues(stats.stavalues2)
|
|
1854
|
+
),
|
|
1855
|
+
_value_type3: this.stavalueKind(
|
|
1856
|
+
_Statistics.safeStavalues(stats.stavalues3)
|
|
1857
|
+
),
|
|
1858
|
+
_value_type4: this.stavalueKind(
|
|
1859
|
+
_Statistics.safeStavalues(stats.stavalues4)
|
|
1860
|
+
),
|
|
1861
|
+
_value_type5: this.stavalueKind(
|
|
1862
|
+
_Statistics.safeStavalues(stats.stavalues5)
|
|
1863
|
+
)
|
|
1737
1864
|
});
|
|
1738
1865
|
}
|
|
1739
1866
|
}
|
|
@@ -2149,14 +2276,16 @@ var _Statistics = class _Statistics {
|
|
|
2149
2276
|
CASE
|
|
2150
2277
|
WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
|
|
2151
2278
|
ELSE 'ASC'
|
|
2152
|
-
END
|
|
2279
|
+
END,
|
|
2280
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2153
2281
|
-- Handle expressions
|
|
2154
2282
|
ELSE
|
|
2155
2283
|
json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
|
|
2156
2284
|
CASE
|
|
2157
2285
|
WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
|
|
2158
2286
|
ELSE 'ASC'
|
|
2159
|
-
END
|
|
2287
|
+
END,
|
|
2288
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2160
2289
|
END
|
|
2161
2290
|
ORDER BY array_position(ix.indkey, k.attnum)
|
|
2162
2291
|
) AS index_columns
|
|
@@ -2168,6 +2297,7 @@ var _Statistics = class _Statistics {
|
|
|
2168
2297
|
JOIN pg_am am ON i.relam = am.oid
|
|
2169
2298
|
LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
|
|
2170
2299
|
LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
|
|
2300
|
+
LEFT JOIN pg_opclass opc ON opc.oid = ix.indclass[k.ordinality - 1]
|
|
2171
2301
|
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
2172
2302
|
WHERE
|
|
2173
2303
|
n.nspname not like 'pg_%' and
|