@query-doctor/core 0.2.5 → 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 +175 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +175 -20
- 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/builder.d.ts +6 -0
- package/dist/sql/builder.d.ts.map +1 -1
- package/dist/sql/walker.d.ts.map +1 -1
- package/package.json +4 -2
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;
|
|
@@ -829,6 +853,9 @@ var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
|
829
853
|
__publicField(this, "isIntrospection", false);
|
|
830
854
|
__publicField(this, "explainFlags", []);
|
|
831
855
|
__publicField(this, "_preamble", 0);
|
|
856
|
+
__publicField(this, "parameters", {});
|
|
857
|
+
// substitution for `limit $1` -> `limit 5`
|
|
858
|
+
__publicField(this, "limitSubstitution");
|
|
832
859
|
}
|
|
833
860
|
get preamble() {
|
|
834
861
|
return this._preamble;
|
|
@@ -862,6 +889,14 @@ var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
|
862
889
|
this.explainFlags = flags;
|
|
863
890
|
return this;
|
|
864
891
|
}
|
|
892
|
+
parameterize(parameters) {
|
|
893
|
+
Object.assign(this.parameters, parameters);
|
|
894
|
+
return this;
|
|
895
|
+
}
|
|
896
|
+
replaceLimit(limit) {
|
|
897
|
+
this.limitSubstitution = limit;
|
|
898
|
+
return this;
|
|
899
|
+
}
|
|
865
900
|
build() {
|
|
866
901
|
let commands = this.generateSetCommands();
|
|
867
902
|
commands += this.generateExplain().query;
|
|
@@ -890,14 +925,28 @@ var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
|
890
925
|
return commands;
|
|
891
926
|
}
|
|
892
927
|
generateExplain() {
|
|
893
|
-
let
|
|
928
|
+
let finalQuery = "";
|
|
894
929
|
if (this.explainFlags.length > 0) {
|
|
895
|
-
|
|
930
|
+
finalQuery += `explain (${this.explainFlags.join(", ")}) `;
|
|
931
|
+
}
|
|
932
|
+
const query = this.substituteQuery();
|
|
933
|
+
const semicolon = query.endsWith(";") ? "" : ";";
|
|
934
|
+
const preamble = finalQuery.length;
|
|
935
|
+
finalQuery += `${query}${semicolon}`;
|
|
936
|
+
return { query: finalQuery, preamble };
|
|
937
|
+
}
|
|
938
|
+
substituteQuery() {
|
|
939
|
+
let query = this.query;
|
|
940
|
+
if (this.limitSubstitution !== void 0) {
|
|
941
|
+
query = query.replace(
|
|
942
|
+
/limit\s+\$\d+/g,
|
|
943
|
+
`limit ${this.limitSubstitution}`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
for (const [key, value] of Object.entries(this.parameters)) {
|
|
947
|
+
query = query.replaceAll(`\\$${key}`, value.toString());
|
|
896
948
|
}
|
|
897
|
-
|
|
898
|
-
const preamble = query.length;
|
|
899
|
-
query += `${this.query}${semicolon}`;
|
|
900
|
-
return { query, preamble };
|
|
949
|
+
return query;
|
|
901
950
|
}
|
|
902
951
|
};
|
|
903
952
|
|
|
@@ -1272,8 +1321,10 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1272
1321
|
* Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
|
|
1273
1322
|
**/
|
|
1274
1323
|
indexesToCreate(rootCandidates) {
|
|
1275
|
-
const
|
|
1324
|
+
const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
|
|
1325
|
+
const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
|
|
1276
1326
|
const nextStage = [];
|
|
1327
|
+
const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
|
|
1277
1328
|
for (const permutation of permutedIndexes.values()) {
|
|
1278
1329
|
const { table: rawTable, schema: rawSchema, columns } = permutation;
|
|
1279
1330
|
const permutations = permutationsWithDescendingLength(columns);
|
|
@@ -1298,6 +1349,41 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1298
1349
|
});
|
|
1299
1350
|
}
|
|
1300
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
|
+
}
|
|
1301
1387
|
return nextStage;
|
|
1302
1388
|
}
|
|
1303
1389
|
toDefinition({
|
|
@@ -1332,6 +1418,47 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1332
1418
|
const colored = make(import_colorette2.green, import_colorette2.yellow, import_colorette2.magenta, import_colorette2.blue);
|
|
1333
1419
|
return { raw, colored };
|
|
1334
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
|
+
}
|
|
1335
1462
|
/**
|
|
1336
1463
|
* Drop indexes that can be dropped. Ignore the ones that can't
|
|
1337
1464
|
*/
|
|
@@ -1648,6 +1775,21 @@ var _Statistics = class _Statistics {
|
|
|
1648
1775
|
}
|
|
1649
1776
|
return "text";
|
|
1650
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
|
+
}
|
|
1651
1793
|
async restoreStats17(tx) {
|
|
1652
1794
|
const warnings = {
|
|
1653
1795
|
tablesNotInExports: [],
|
|
@@ -1699,16 +1841,26 @@ var _Statistics = class _Statistics {
|
|
|
1699
1841
|
stanumbers3: stats.stanumbers3,
|
|
1700
1842
|
stanumbers4: stats.stanumbers4,
|
|
1701
1843
|
stanumbers5: stats.stanumbers5,
|
|
1702
|
-
stavalues1: stats.stavalues1,
|
|
1703
|
-
stavalues2: stats.stavalues2,
|
|
1704
|
-
stavalues3: stats.stavalues3,
|
|
1705
|
-
stavalues4: stats.stavalues4,
|
|
1706
|
-
stavalues5: stats.stavalues5,
|
|
1707
|
-
_value_type1: this.stavalueKind(
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
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
|
+
)
|
|
1712
1864
|
});
|
|
1713
1865
|
}
|
|
1714
1866
|
}
|
|
@@ -2124,14 +2276,16 @@ var _Statistics = class _Statistics {
|
|
|
2124
2276
|
CASE
|
|
2125
2277
|
WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
|
|
2126
2278
|
ELSE 'ASC'
|
|
2127
|
-
END
|
|
2279
|
+
END,
|
|
2280
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2128
2281
|
-- Handle expressions
|
|
2129
2282
|
ELSE
|
|
2130
2283
|
json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
|
|
2131
2284
|
CASE
|
|
2132
2285
|
WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
|
|
2133
2286
|
ELSE 'ASC'
|
|
2134
|
-
END
|
|
2287
|
+
END,
|
|
2288
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2135
2289
|
END
|
|
2136
2290
|
ORDER BY array_position(ix.indkey, k.attnum)
|
|
2137
2291
|
) AS index_columns
|
|
@@ -2143,6 +2297,7 @@ var _Statistics = class _Statistics {
|
|
|
2143
2297
|
JOIN pg_am am ON i.relam = am.oid
|
|
2144
2298
|
LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
|
|
2145
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]
|
|
2146
2301
|
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
2147
2302
|
WHERE
|
|
2148
2303
|
n.nspname not like 'pg_%' and
|