@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.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;
|
|
@@ -780,6 +804,9 @@ var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
|
780
804
|
__publicField(this, "isIntrospection", false);
|
|
781
805
|
__publicField(this, "explainFlags", []);
|
|
782
806
|
__publicField(this, "_preamble", 0);
|
|
807
|
+
__publicField(this, "parameters", {});
|
|
808
|
+
// substitution for `limit $1` -> `limit 5`
|
|
809
|
+
__publicField(this, "limitSubstitution");
|
|
783
810
|
}
|
|
784
811
|
get preamble() {
|
|
785
812
|
return this._preamble;
|
|
@@ -813,6 +840,14 @@ var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
|
813
840
|
this.explainFlags = flags;
|
|
814
841
|
return this;
|
|
815
842
|
}
|
|
843
|
+
parameterize(parameters) {
|
|
844
|
+
Object.assign(this.parameters, parameters);
|
|
845
|
+
return this;
|
|
846
|
+
}
|
|
847
|
+
replaceLimit(limit) {
|
|
848
|
+
this.limitSubstitution = limit;
|
|
849
|
+
return this;
|
|
850
|
+
}
|
|
816
851
|
build() {
|
|
817
852
|
let commands = this.generateSetCommands();
|
|
818
853
|
commands += this.generateExplain().query;
|
|
@@ -841,14 +876,28 @@ var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
|
841
876
|
return commands;
|
|
842
877
|
}
|
|
843
878
|
generateExplain() {
|
|
844
|
-
let
|
|
879
|
+
let finalQuery = "";
|
|
845
880
|
if (this.explainFlags.length > 0) {
|
|
846
|
-
|
|
881
|
+
finalQuery += `explain (${this.explainFlags.join(", ")}) `;
|
|
882
|
+
}
|
|
883
|
+
const query = this.substituteQuery();
|
|
884
|
+
const semicolon = query.endsWith(";") ? "" : ";";
|
|
885
|
+
const preamble = finalQuery.length;
|
|
886
|
+
finalQuery += `${query}${semicolon}`;
|
|
887
|
+
return { query: finalQuery, preamble };
|
|
888
|
+
}
|
|
889
|
+
substituteQuery() {
|
|
890
|
+
let query = this.query;
|
|
891
|
+
if (this.limitSubstitution !== void 0) {
|
|
892
|
+
query = query.replace(
|
|
893
|
+
/limit\s+\$\d+/g,
|
|
894
|
+
`limit ${this.limitSubstitution}`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
for (const [key, value] of Object.entries(this.parameters)) {
|
|
898
|
+
query = query.replaceAll(`\\$${key}`, value.toString());
|
|
847
899
|
}
|
|
848
|
-
|
|
849
|
-
const preamble = query.length;
|
|
850
|
-
query += `${this.query}${semicolon}`;
|
|
851
|
-
return { query, preamble };
|
|
900
|
+
return query;
|
|
852
901
|
}
|
|
853
902
|
};
|
|
854
903
|
|
|
@@ -1223,8 +1272,10 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1223
1272
|
* Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
|
|
1224
1273
|
**/
|
|
1225
1274
|
indexesToCreate(rootCandidates) {
|
|
1226
|
-
const
|
|
1275
|
+
const btreeCandidates = rootCandidates.filter((c) => !c.jsonbOperator);
|
|
1276
|
+
const ginCandidates = rootCandidates.filter((c) => c.jsonbOperator);
|
|
1227
1277
|
const nextStage = [];
|
|
1278
|
+
const permutedIndexes = this.groupPotentialIndexColumnsByTable(btreeCandidates);
|
|
1228
1279
|
for (const permutation of permutedIndexes.values()) {
|
|
1229
1280
|
const { table: rawTable, schema: rawSchema, columns } = permutation;
|
|
1230
1281
|
const permutations = permutationsWithDescendingLength(columns);
|
|
@@ -1249,6 +1300,41 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1249
1300
|
});
|
|
1250
1301
|
}
|
|
1251
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
|
+
}
|
|
1252
1338
|
return nextStage;
|
|
1253
1339
|
}
|
|
1254
1340
|
toDefinition({
|
|
@@ -1283,6 +1369,47 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
1283
1369
|
const colored = make(green, yellow, magenta, blue2);
|
|
1284
1370
|
return { raw, colored };
|
|
1285
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
|
+
}
|
|
1286
1413
|
/**
|
|
1287
1414
|
* Drop indexes that can be dropped. Ignore the ones that can't
|
|
1288
1415
|
*/
|
|
@@ -1599,6 +1726,21 @@ var _Statistics = class _Statistics {
|
|
|
1599
1726
|
}
|
|
1600
1727
|
return "text";
|
|
1601
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
|
+
}
|
|
1602
1744
|
async restoreStats17(tx) {
|
|
1603
1745
|
const warnings = {
|
|
1604
1746
|
tablesNotInExports: [],
|
|
@@ -1650,16 +1792,26 @@ var _Statistics = class _Statistics {
|
|
|
1650
1792
|
stanumbers3: stats.stanumbers3,
|
|
1651
1793
|
stanumbers4: stats.stanumbers4,
|
|
1652
1794
|
stanumbers5: stats.stanumbers5,
|
|
1653
|
-
stavalues1: stats.stavalues1,
|
|
1654
|
-
stavalues2: stats.stavalues2,
|
|
1655
|
-
stavalues3: stats.stavalues3,
|
|
1656
|
-
stavalues4: stats.stavalues4,
|
|
1657
|
-
stavalues5: stats.stavalues5,
|
|
1658
|
-
_value_type1: this.stavalueKind(
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
+
)
|
|
1663
1815
|
});
|
|
1664
1816
|
}
|
|
1665
1817
|
}
|
|
@@ -2075,14 +2227,16 @@ var _Statistics = class _Statistics {
|
|
|
2075
2227
|
CASE
|
|
2076
2228
|
WHEN (indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 THEN 'DESC'
|
|
2077
2229
|
ELSE 'ASC'
|
|
2078
|
-
END
|
|
2230
|
+
END,
|
|
2231
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2079
2232
|
-- Handle expressions
|
|
2080
2233
|
ELSE
|
|
2081
2234
|
json_build_object('name', pg_get_expr((ix.indexprs)::pg_node_tree, t.oid), 'order',
|
|
2082
2235
|
CASE
|
|
2083
2236
|
WHEN (indoption[array_position(ix.indkey, k.attnum)] & 1) = 1 THEN 'DESC'
|
|
2084
2237
|
ELSE 'ASC'
|
|
2085
|
-
END
|
|
2238
|
+
END,
|
|
2239
|
+
'opclass', CASE WHEN opc.opcdefault THEN NULL ELSE opc.opcname END)
|
|
2086
2240
|
END
|
|
2087
2241
|
ORDER BY array_position(ix.indkey, k.attnum)
|
|
2088
2242
|
) AS index_columns
|
|
@@ -2094,6 +2248,7 @@ var _Statistics = class _Statistics {
|
|
|
2094
2248
|
JOIN pg_am am ON i.relam = am.oid
|
|
2095
2249
|
LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality) ON true
|
|
2096
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]
|
|
2097
2252
|
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
2098
2253
|
WHERE
|
|
2099
2254
|
n.nspname not like 'pg_%' and
|