@tabularis/explain 0.2.0 → 0.3.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/README.md +6 -3
- package/dist/{diagnostics-DPSgsjFk.d.ts → diagnostics-DYKMtJZS.d.ts} +4 -4
- package/dist/flow.d.ts +1 -1
- package/dist/flow.js +2 -8
- package/dist/index.d.ts +157 -19
- package/dist/index.js +529 -47
- package/dist/react.d.ts +22 -8
- package/dist/react.js +774 -311
- package/dist/tableColumns-BWoGh7tK.d.ts +30 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -304,13 +304,17 @@ function parseMysqlQueryBlock(value, ids) {
|
|
|
304
304
|
if (item["table"] !== void 0) {
|
|
305
305
|
children.push(parseMysqlQueryBlock(item, ids));
|
|
306
306
|
} else if (item["read_sorted_file"] !== void 0) {
|
|
307
|
-
children.push(
|
|
307
|
+
children.push(
|
|
308
|
+
parseMariadbWrapper(item["read_sorted_file"], "Read Sorted File", ids)
|
|
309
|
+
);
|
|
308
310
|
} else if (asObject(item["filesort"]) !== null) {
|
|
309
311
|
children.push(parseMariadbFilesort(item["filesort"], ids));
|
|
310
312
|
} else if (item["temporary_table"] !== void 0) {
|
|
311
313
|
children.push(parseMariadbTemporaryTable(item["temporary_table"], ids));
|
|
312
314
|
} else if (item["materialized"] !== void 0) {
|
|
313
|
-
children.push(
|
|
315
|
+
children.push(
|
|
316
|
+
parseMariadbWrapper(item["materialized"], "Materialized Subquery", ids)
|
|
317
|
+
);
|
|
314
318
|
} else if (item["buffer_result"] !== void 0) {
|
|
315
319
|
children.push(parseMariadbWrapper(item["buffer_result"], "Buffer Result", ids));
|
|
316
320
|
}
|
|
@@ -698,9 +702,7 @@ function parsePgPlanNode(node, ids) {
|
|
|
698
702
|
extra[key] = value;
|
|
699
703
|
}
|
|
700
704
|
}
|
|
701
|
-
const children = (asArray(obj["Plans"]) ?? []).map(
|
|
702
|
-
(child) => parsePgPlanNode(child, ids)
|
|
703
|
-
);
|
|
705
|
+
const children = (asArray(obj["Plans"]) ?? []).map((child) => parsePgPlanNode(child, ids));
|
|
704
706
|
return createExplainNode(id, {
|
|
705
707
|
node_type: asString(obj["Node Type"]) ?? "Unknown",
|
|
706
708
|
relation: asString(obj["Relation Name"]),
|
|
@@ -1053,16 +1055,12 @@ var BUILTIN_EXPLAIN_PARSERS = Object.freeze([
|
|
|
1053
1055
|
Object.freeze({
|
|
1054
1056
|
engine: "mysql",
|
|
1055
1057
|
format: "mysql-tabular-rows",
|
|
1056
|
-
parse: (payload) => parseMysqlTabularRows(
|
|
1057
|
-
parseJsonRows(payload, "mysql-tabular-rows")
|
|
1058
|
-
)
|
|
1058
|
+
parse: (payload) => parseMysqlTabularRows(parseJsonRows(payload, "mysql-tabular-rows"))
|
|
1059
1059
|
}),
|
|
1060
1060
|
Object.freeze({
|
|
1061
1061
|
engine: "sqlite",
|
|
1062
1062
|
format: "sqlite-eqp-rows",
|
|
1063
|
-
parse: (payload) => parseSqliteEqpRows(
|
|
1064
|
-
parseJsonRows(payload, "sqlite-eqp-rows")
|
|
1065
|
-
)
|
|
1063
|
+
parse: (payload) => parseSqliteEqpRows(parseJsonRows(payload, "sqlite-eqp-rows"))
|
|
1066
1064
|
})
|
|
1067
1065
|
]);
|
|
1068
1066
|
|
|
@@ -1144,19 +1142,19 @@ function explainEngineFromDriverName(name) {
|
|
|
1144
1142
|
);
|
|
1145
1143
|
return parser?.engine ?? null;
|
|
1146
1144
|
}
|
|
1147
|
-
function detectFormat(raw) {
|
|
1148
|
-
return detectFormatFor(raw, null);
|
|
1145
|
+
async function detectFormat(raw) {
|
|
1146
|
+
return await detectFormatFor(raw, null);
|
|
1149
1147
|
}
|
|
1150
|
-
function detectFormatFor(raw, engine) {
|
|
1148
|
+
async function detectFormatFor(raw, engine) {
|
|
1151
1149
|
switch (engine) {
|
|
1152
1150
|
case "postgres":
|
|
1153
|
-
return detectPostgresFormat(raw);
|
|
1151
|
+
return Promise.resolve(detectPostgresFormat(raw));
|
|
1154
1152
|
case "mysql":
|
|
1155
|
-
if (looksLikeJson(raw)) return "mysql-json";
|
|
1153
|
+
if (looksLikeJson(raw)) return Promise.resolve("mysql-json");
|
|
1156
1154
|
if (raw.trim() === "") {
|
|
1157
1155
|
throw new Error("Unsupported EXPLAIN file format: input is empty");
|
|
1158
1156
|
}
|
|
1159
|
-
return "mysql-text";
|
|
1157
|
+
return Promise.resolve("mysql-text");
|
|
1160
1158
|
case "sqlite":
|
|
1161
1159
|
throw new Error(
|
|
1162
1160
|
"SQLite EXPLAIN QUERY PLAN has no text form here: pass its (id, parent, detail) rows to buildSqliteTree"
|
|
@@ -1164,14 +1162,14 @@ function detectFormatFor(raw, engine) {
|
|
|
1164
1162
|
case null: {
|
|
1165
1163
|
const builtinFormat = detectPostgresFormatOrNull(raw);
|
|
1166
1164
|
if (builtinFormat !== null) return builtinFormat;
|
|
1167
|
-
const customFormat = sniffRegisteredFormat(raw, null);
|
|
1165
|
+
const customFormat = await sniffRegisteredFormat(raw, null);
|
|
1168
1166
|
if (customFormat !== null) return customFormat;
|
|
1169
1167
|
throw new Error(
|
|
1170
1168
|
"Unsupported EXPLAIN file format: expected Postgres JSON or text output"
|
|
1171
1169
|
);
|
|
1172
1170
|
}
|
|
1173
1171
|
default: {
|
|
1174
|
-
const format = sniffRegisteredFormat(raw, engine);
|
|
1172
|
+
const format = await sniffRegisteredFormat(raw, engine);
|
|
1175
1173
|
if (format !== null) return format;
|
|
1176
1174
|
throw new Error(`Unsupported EXPLAIN file format for engine '${engine}'`);
|
|
1177
1175
|
}
|
|
@@ -1180,16 +1178,14 @@ function detectFormatFor(raw, engine) {
|
|
|
1180
1178
|
function detectPostgresFormat(raw) {
|
|
1181
1179
|
const format = detectPostgresFormatOrNull(raw);
|
|
1182
1180
|
if (format !== null) return format;
|
|
1183
|
-
throw new Error(
|
|
1184
|
-
"Unsupported EXPLAIN file format: expected Postgres JSON or text output"
|
|
1185
|
-
);
|
|
1181
|
+
throw new Error("Unsupported EXPLAIN file format: expected Postgres JSON or text output");
|
|
1186
1182
|
}
|
|
1187
1183
|
function detectPostgresFormatOrNull(raw) {
|
|
1188
1184
|
if (looksLikeJson(raw)) return "postgres-json";
|
|
1189
1185
|
if (looksLikePostgresText(raw)) return "postgres-text";
|
|
1190
1186
|
return null;
|
|
1191
1187
|
}
|
|
1192
|
-
function sniffRegisteredFormat(raw, engine) {
|
|
1188
|
+
async function sniffRegisteredFormat(raw, engine) {
|
|
1193
1189
|
const normalizedEngine = engine?.trim().toLowerCase() ?? null;
|
|
1194
1190
|
for (const parser of listExplainParsers()) {
|
|
1195
1191
|
if (normalizedEngine === null && BUILTIN_SOURCE_FORMATS.has(parser.format)) {
|
|
@@ -1200,7 +1196,7 @@ function sniffRegisteredFormat(raw, engine) {
|
|
|
1200
1196
|
}
|
|
1201
1197
|
if (parser.sniff === void 0) continue;
|
|
1202
1198
|
try {
|
|
1203
|
-
if (parser.sniff(raw)) return parser.format;
|
|
1199
|
+
if (await parser.sniff(raw)) return parser.format;
|
|
1204
1200
|
} catch {
|
|
1205
1201
|
}
|
|
1206
1202
|
}
|
|
@@ -1216,8 +1212,9 @@ function looksLikePostgresText(raw) {
|
|
|
1216
1212
|
function parseExplain(raw) {
|
|
1217
1213
|
return parseExplainFor(raw, null);
|
|
1218
1214
|
}
|
|
1219
|
-
function parseExplainFor(raw, engine) {
|
|
1220
|
-
|
|
1215
|
+
async function parseExplainFor(raw, engine) {
|
|
1216
|
+
const format = await detectFormatFor(raw, engine);
|
|
1217
|
+
return await parserFor(format).parse(raw);
|
|
1221
1218
|
}
|
|
1222
1219
|
function withSourceLabel(plan, name) {
|
|
1223
1220
|
if (plan.original_query === "") {
|
|
@@ -1227,14 +1224,14 @@ function withSourceLabel(plan, name) {
|
|
|
1227
1224
|
}
|
|
1228
1225
|
|
|
1229
1226
|
// src/raw.ts
|
|
1230
|
-
function parseRawExplain(raw) {
|
|
1227
|
+
async function parseRawExplain(raw) {
|
|
1231
1228
|
const parser = getExplainParser(raw.format);
|
|
1232
1229
|
if (parser === null) {
|
|
1233
1230
|
throw new Error(
|
|
1234
1231
|
`No EXPLAIN parser registered for format '${raw.format}' (engine '${raw.engine}'). Import the parser package for '${raw.engine}' before parsing.`
|
|
1235
1232
|
);
|
|
1236
1233
|
}
|
|
1237
|
-
const plan = parser.parse(raw.payload);
|
|
1234
|
+
const plan = await parser.parse(raw.payload);
|
|
1238
1235
|
return {
|
|
1239
1236
|
...plan,
|
|
1240
1237
|
driver: raw.engine,
|
|
@@ -1242,7 +1239,7 @@ function parseRawExplain(raw) {
|
|
|
1242
1239
|
};
|
|
1243
1240
|
}
|
|
1244
1241
|
function resolveExplainOutput(output) {
|
|
1245
|
-
return output.kind === "plan" ? output.plan : parseRawExplain(output.raw);
|
|
1242
|
+
return output.kind === "plan" ? Promise.resolve(output.plan) : parseRawExplain(output.raw);
|
|
1246
1243
|
}
|
|
1247
1244
|
|
|
1248
1245
|
// src/metrics.ts
|
|
@@ -1323,16 +1320,10 @@ function computeExplainMetrics(plan) {
|
|
|
1323
1320
|
let maxExclusiveBuffers = 0;
|
|
1324
1321
|
let totalExclusiveTimeMs = 0;
|
|
1325
1322
|
for (const metrics of order) {
|
|
1326
|
-
maxExclusiveTimeMs = Math.max(
|
|
1327
|
-
maxExclusiveTimeMs,
|
|
1328
|
-
metrics.exclusiveTimeMs ?? 0
|
|
1329
|
-
);
|
|
1323
|
+
maxExclusiveTimeMs = Math.max(maxExclusiveTimeMs, metrics.exclusiveTimeMs ?? 0);
|
|
1330
1324
|
maxExclusiveCost = Math.max(maxExclusiveCost, metrics.exclusiveCost ?? 0);
|
|
1331
1325
|
maxTotalRows = Math.max(maxTotalRows, metrics.totalRows ?? 0);
|
|
1332
|
-
maxExclusiveBuffers = Math.max(
|
|
1333
|
-
maxExclusiveBuffers,
|
|
1334
|
-
metrics.exclusiveBuffers ?? 0
|
|
1335
|
-
);
|
|
1326
|
+
maxExclusiveBuffers = Math.max(maxExclusiveBuffers, metrics.exclusiveBuffers ?? 0);
|
|
1336
1327
|
totalExclusiveTimeMs += metrics.exclusiveTimeMs ?? 0;
|
|
1337
1328
|
}
|
|
1338
1329
|
if (totalExclusiveTimeMs > 0) {
|
|
@@ -1481,7 +1472,7 @@ function getRowEstimateRatio(node) {
|
|
|
1481
1472
|
}
|
|
1482
1473
|
return node.actual_rows / node.plan_rows;
|
|
1483
1474
|
}
|
|
1484
|
-
function
|
|
1475
|
+
function getRowEstimateGap(node) {
|
|
1485
1476
|
const ratio = getRowEstimateRatio(node);
|
|
1486
1477
|
if (ratio == null) {
|
|
1487
1478
|
return null;
|
|
@@ -1527,7 +1518,7 @@ function getExplainPlanSummary(plan, metrics) {
|
|
|
1527
1518
|
};
|
|
1528
1519
|
}
|
|
1529
1520
|
const ratio = getRowEstimateRatio(node);
|
|
1530
|
-
const magnitude =
|
|
1521
|
+
const magnitude = getRowEstimateGap(node);
|
|
1531
1522
|
if (ratio != null && magnitude != null && (largestRowMismatchNode == null || magnitude > largestRowMismatchNode.value)) {
|
|
1532
1523
|
largestRowMismatchNode = {
|
|
1533
1524
|
nodeId: node.id,
|
|
@@ -1571,14 +1562,33 @@ function getExplainDriverLegend(plan) {
|
|
|
1571
1562
|
"editor.visualExplain.mysqlEstimateLegend2"
|
|
1572
1563
|
];
|
|
1573
1564
|
case "sqlite":
|
|
1574
|
-
return [
|
|
1575
|
-
"editor.visualExplain.sqliteLegend1",
|
|
1576
|
-
"editor.visualExplain.sqliteLegend2"
|
|
1577
|
-
];
|
|
1565
|
+
return ["editor.visualExplain.sqliteLegend1", "editor.visualExplain.sqliteLegend2"];
|
|
1578
1566
|
default:
|
|
1579
1567
|
return [];
|
|
1580
1568
|
}
|
|
1581
1569
|
}
|
|
1570
|
+
function getExplainTableRow(node, hasAnalyzeData, nodeMetrics, fallbackIndex = 1) {
|
|
1571
|
+
return {
|
|
1572
|
+
index: nodeMetrics?.index ?? fallbackIndex,
|
|
1573
|
+
depth: nodeMetrics?.depth ?? 0,
|
|
1574
|
+
nodeType: node.node_type,
|
|
1575
|
+
relation: node.relation ?? null,
|
|
1576
|
+
startupCost: node.startup_cost ?? null,
|
|
1577
|
+
totalCost: node.total_cost ?? null,
|
|
1578
|
+
planRows: node.plan_rows ?? null,
|
|
1579
|
+
actualRows: hasAnalyzeData ? node.actual_rows ?? null : null,
|
|
1580
|
+
actualLoops: hasAnalyzeData ? node.actual_loops ?? null : null,
|
|
1581
|
+
exclusiveTimeMs: hasAnalyzeData ? nodeMetrics?.exclusiveTimeMs ?? null : null,
|
|
1582
|
+
estimateGap: getRowEstimateGap(node),
|
|
1583
|
+
filter: node.filter ?? null
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function getExplainTableRows(plan, metrics) {
|
|
1587
|
+
const planMetrics = metrics ?? computeExplainMetrics(plan);
|
|
1588
|
+
return flattenExplainNodes(plan.root).map(
|
|
1589
|
+
(node, i) => getExplainTableRow(node, plan.has_analyze_data, planMetrics.byId.get(node.id), i + 1)
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1582
1592
|
|
|
1583
1593
|
// src/diagnostics.ts
|
|
1584
1594
|
var EXPLAIN_DIAGNOSTIC_THRESHOLDS = {
|
|
@@ -1764,9 +1774,7 @@ function getNodeDiagnostics(node, metrics) {
|
|
|
1764
1774
|
value: formatRows(node.buffers_read)
|
|
1765
1775
|
});
|
|
1766
1776
|
}
|
|
1767
|
-
return diagnostics.sort(
|
|
1768
|
-
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
|
|
1769
|
-
);
|
|
1777
|
+
return diagnostics.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
1770
1778
|
}
|
|
1771
1779
|
function getPlanDiagnostics(plan, metrics) {
|
|
1772
1780
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -1910,4 +1918,478 @@ function compareByTimeThenCount(a, b) {
|
|
|
1910
1918
|
return (b.exclusiveTimeMs ?? 0) - (a.exclusiveTimeMs ?? 0) || b.count - a.count || a.nodeType.localeCompare(b.nodeType);
|
|
1911
1919
|
}
|
|
1912
1920
|
|
|
1913
|
-
|
|
1921
|
+
// src/tableColumns.ts
|
|
1922
|
+
var EXPLAIN_TABLE_COLUMNS = [
|
|
1923
|
+
"operation",
|
|
1924
|
+
"relation",
|
|
1925
|
+
"cost",
|
|
1926
|
+
"estRows",
|
|
1927
|
+
"actualRows",
|
|
1928
|
+
"loops",
|
|
1929
|
+
"selfTime",
|
|
1930
|
+
"estimateGap",
|
|
1931
|
+
"filter"
|
|
1932
|
+
];
|
|
1933
|
+
var EXPLAIN_TABLE_ANALYZE_COLUMNS = /* @__PURE__ */ new Set([
|
|
1934
|
+
"actualRows",
|
|
1935
|
+
"loops"
|
|
1936
|
+
]);
|
|
1937
|
+
function isExplainTableColumnId(value) {
|
|
1938
|
+
return typeof value === "string" && EXPLAIN_TABLE_COLUMNS.includes(value);
|
|
1939
|
+
}
|
|
1940
|
+
function normalizeExplainTableColumnOrder(saved) {
|
|
1941
|
+
const order = [];
|
|
1942
|
+
if (Array.isArray(saved)) {
|
|
1943
|
+
for (const id of saved) {
|
|
1944
|
+
if (isExplainTableColumnId(id) && !order.includes(id)) order.push(id);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
EXPLAIN_TABLE_COLUMNS.forEach((id, defaultIndex) => {
|
|
1948
|
+
if (order.includes(id)) return;
|
|
1949
|
+
const previous = EXPLAIN_TABLE_COLUMNS.slice(0, defaultIndex).reverse();
|
|
1950
|
+
const anchor = previous.find((candidate) => order.includes(candidate));
|
|
1951
|
+
order.splice(anchor ? order.indexOf(anchor) + 1 : 0, 0, id);
|
|
1952
|
+
});
|
|
1953
|
+
return order;
|
|
1954
|
+
}
|
|
1955
|
+
function moveExplainTableColumn(order, column, target) {
|
|
1956
|
+
const from = order.indexOf(column);
|
|
1957
|
+
const to = order.indexOf(target);
|
|
1958
|
+
if (from === -1 || to === -1 || from === to) return order;
|
|
1959
|
+
const next = order.filter((id) => id !== column);
|
|
1960
|
+
next.splice(to, 0, column);
|
|
1961
|
+
return next;
|
|
1962
|
+
}
|
|
1963
|
+
function shiftExplainTableColumn(order, column, direction, visible = order) {
|
|
1964
|
+
const shown = order.filter((id) => visible.includes(id));
|
|
1965
|
+
const at = shown.indexOf(column);
|
|
1966
|
+
const neighbour = at === -1 ? void 0 : shown[at + direction];
|
|
1967
|
+
return neighbour ? moveExplainTableColumn(order, column, neighbour) : order;
|
|
1968
|
+
}
|
|
1969
|
+
function isDefaultExplainTableColumnOrder(order) {
|
|
1970
|
+
return order.length === EXPLAIN_TABLE_COLUMNS.length && order.every((id, index) => id === EXPLAIN_TABLE_COLUMNS[index]);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// src/nodeDetails.ts
|
|
1974
|
+
var DEFAULT_LABELS = {
|
|
1975
|
+
"editor.visualExplain.nodeType": "Operation",
|
|
1976
|
+
"editor.visualExplain.relation": "Table",
|
|
1977
|
+
"editor.visualExplain.cost": "Cost",
|
|
1978
|
+
"editor.visualExplain.selfCost": "Self Cost",
|
|
1979
|
+
"editor.visualExplain.estRows": "Est. Rows",
|
|
1980
|
+
"editor.visualExplain.filter": "Filter",
|
|
1981
|
+
"editor.visualExplain.indexCondition": "Index Cond.",
|
|
1982
|
+
"editor.visualExplain.joinType": "Join Type",
|
|
1983
|
+
"editor.visualExplain.hashCondition": "Hash Cond.",
|
|
1984
|
+
"editor.visualExplain.actualRows": "Actual Rows",
|
|
1985
|
+
"editor.visualExplain.rowsAllLoops": "Rows (All Loops)",
|
|
1986
|
+
"editor.visualExplain.selfTime": "Self Time",
|
|
1987
|
+
"editor.visualExplain.totalTime": "Total Time",
|
|
1988
|
+
"editor.visualExplain.timePerLoop": "Time per Loop",
|
|
1989
|
+
"editor.visualExplain.loops": "Loops",
|
|
1990
|
+
"editor.visualExplain.buffersHit": "Buffers Hit",
|
|
1991
|
+
"editor.visualExplain.buffersRead": "Buffers Read",
|
|
1992
|
+
"editor.visualExplain.general": "General",
|
|
1993
|
+
"editor.visualExplain.analyzeData": "Analyze Data",
|
|
1994
|
+
"editor.visualExplain.extraDetails": "Extra",
|
|
1995
|
+
"editor.visualExplain.diagnostics.title": "Diagnostics"
|
|
1996
|
+
};
|
|
1997
|
+
function getExplainNodeDetailSections(node, options = {}) {
|
|
1998
|
+
const { hasAnalyzeData = false, metrics = null, diagnostics = [], t } = options;
|
|
1999
|
+
const tr = (key, fallback) => {
|
|
2000
|
+
if (t) {
|
|
2001
|
+
return t(key, fallback ?? DEFAULT_LABELS[key] ?? key);
|
|
2002
|
+
}
|
|
2003
|
+
return fallback ?? DEFAULT_LABELS[key] ?? key;
|
|
2004
|
+
};
|
|
2005
|
+
const generalEntries = [
|
|
2006
|
+
{ label: tr("editor.visualExplain.nodeType", "Operation"), value: node.node_type },
|
|
2007
|
+
...node.relation ? [{ label: tr("editor.visualExplain.relation", "Table"), value: node.relation }] : [],
|
|
2008
|
+
...node.startup_cost != null && node.total_cost != null ? [
|
|
2009
|
+
{
|
|
2010
|
+
label: tr("editor.visualExplain.cost", "Cost"),
|
|
2011
|
+
value: `${formatCost(node.startup_cost)} - ${formatCost(node.total_cost)}`
|
|
2012
|
+
}
|
|
2013
|
+
] : node.total_cost != null ? [
|
|
2014
|
+
{
|
|
2015
|
+
label: tr("editor.visualExplain.cost", "Cost"),
|
|
2016
|
+
value: formatCost(node.total_cost)
|
|
2017
|
+
}
|
|
2018
|
+
] : [],
|
|
2019
|
+
...metrics?.exclusiveCost != null ? [
|
|
2020
|
+
{
|
|
2021
|
+
label: tr("editor.visualExplain.selfCost", "Self Cost"),
|
|
2022
|
+
value: formatCost(metrics.exclusiveCost)
|
|
2023
|
+
}
|
|
2024
|
+
] : [],
|
|
2025
|
+
...node.plan_rows != null ? [
|
|
2026
|
+
{
|
|
2027
|
+
label: tr("editor.visualExplain.estRows", "Est. Rows"),
|
|
2028
|
+
value: formatRows(node.plan_rows)
|
|
2029
|
+
}
|
|
2030
|
+
] : [],
|
|
2031
|
+
...node.filter ? [{ label: tr("editor.visualExplain.filter", "Filter"), value: node.filter }] : [],
|
|
2032
|
+
...node.index_condition ? [
|
|
2033
|
+
{
|
|
2034
|
+
label: tr("editor.visualExplain.indexCondition", "Index Cond."),
|
|
2035
|
+
value: node.index_condition
|
|
2036
|
+
}
|
|
2037
|
+
] : [],
|
|
2038
|
+
...node.join_type ? [{ label: tr("editor.visualExplain.joinType", "Join Type"), value: node.join_type }] : [],
|
|
2039
|
+
...node.hash_condition ? [
|
|
2040
|
+
{
|
|
2041
|
+
label: tr("editor.visualExplain.hashCondition", "Hash Cond."),
|
|
2042
|
+
value: node.hash_condition
|
|
2043
|
+
}
|
|
2044
|
+
] : []
|
|
2045
|
+
];
|
|
2046
|
+
const analyzeEntries = hasAnalyzeData ? [
|
|
2047
|
+
...node.actual_rows != null ? [
|
|
2048
|
+
{
|
|
2049
|
+
label: tr("editor.visualExplain.actualRows", "Actual Rows"),
|
|
2050
|
+
value: formatRows(node.actual_rows)
|
|
2051
|
+
}
|
|
2052
|
+
] : [],
|
|
2053
|
+
...metrics?.totalRows != null && node.actual_loops != null && node.actual_loops > 1 ? [
|
|
2054
|
+
{
|
|
2055
|
+
label: tr("editor.visualExplain.rowsAllLoops", "Rows (All Loops)"),
|
|
2056
|
+
value: formatRows(metrics.totalRows)
|
|
2057
|
+
}
|
|
2058
|
+
] : [],
|
|
2059
|
+
...metrics?.exclusiveTimeMs != null ? [
|
|
2060
|
+
{
|
|
2061
|
+
label: tr("editor.visualExplain.selfTime", "Self Time"),
|
|
2062
|
+
value: metrics.timeShare != null ? `${formatTime(metrics.exclusiveTimeMs)} (${Math.round(metrics.timeShare * 100)}%)` : formatTime(metrics.exclusiveTimeMs)
|
|
2063
|
+
}
|
|
2064
|
+
] : [],
|
|
2065
|
+
...metrics?.inclusiveTimeMs != null ? [
|
|
2066
|
+
{
|
|
2067
|
+
label: tr("editor.visualExplain.totalTime", "Total Time"),
|
|
2068
|
+
value: formatTime(metrics.inclusiveTimeMs)
|
|
2069
|
+
}
|
|
2070
|
+
] : [],
|
|
2071
|
+
...node.actual_time_ms != null ? [
|
|
2072
|
+
{
|
|
2073
|
+
label: tr("editor.visualExplain.timePerLoop", "Time per Loop"),
|
|
2074
|
+
value: formatTime(node.actual_time_ms)
|
|
2075
|
+
}
|
|
2076
|
+
] : [],
|
|
2077
|
+
...node.actual_loops != null ? [
|
|
2078
|
+
{
|
|
2079
|
+
label: tr("editor.visualExplain.loops", "Loops"),
|
|
2080
|
+
value: String(node.actual_loops)
|
|
2081
|
+
}
|
|
2082
|
+
] : [],
|
|
2083
|
+
...node.buffers_hit != null ? [
|
|
2084
|
+
{
|
|
2085
|
+
label: tr("editor.visualExplain.buffersHit", "Buffers Hit"),
|
|
2086
|
+
value: String(node.buffers_hit)
|
|
2087
|
+
}
|
|
2088
|
+
] : [],
|
|
2089
|
+
...node.buffers_read != null ? [
|
|
2090
|
+
{
|
|
2091
|
+
label: tr("editor.visualExplain.buffersRead", "Buffers Read"),
|
|
2092
|
+
value: String(node.buffers_read)
|
|
2093
|
+
}
|
|
2094
|
+
] : []
|
|
2095
|
+
] : [];
|
|
2096
|
+
const extraEntries = Object.entries(node.extra ?? {}).map(
|
|
2097
|
+
([key, value]) => ({
|
|
2098
|
+
label: key,
|
|
2099
|
+
value: typeof value === "string" ? value : JSON.stringify(value)
|
|
2100
|
+
})
|
|
2101
|
+
);
|
|
2102
|
+
const sections = [];
|
|
2103
|
+
if (generalEntries.length > 0) {
|
|
2104
|
+
sections.push({
|
|
2105
|
+
title: tr("editor.visualExplain.general", "General"),
|
|
2106
|
+
titleKey: "editor.visualExplain.general",
|
|
2107
|
+
entries: generalEntries
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
if (analyzeEntries.length > 0) {
|
|
2111
|
+
sections.push({
|
|
2112
|
+
title: tr("editor.visualExplain.analyzeData", "Analyze Data"),
|
|
2113
|
+
titleKey: "editor.visualExplain.analyzeData",
|
|
2114
|
+
entries: analyzeEntries
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
if (extraEntries.length > 0) {
|
|
2118
|
+
sections.push({
|
|
2119
|
+
title: tr("editor.visualExplain.extraDetails", "Extra"),
|
|
2120
|
+
titleKey: "editor.visualExplain.extraDetails",
|
|
2121
|
+
entries: extraEntries
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
if (diagnostics && diagnostics.length > 0) {
|
|
2125
|
+
const diagEntries = diagnostics.map((d) => ({
|
|
2126
|
+
label: tr(d.labelKey, d.kind),
|
|
2127
|
+
value: d.value ? `${tr(d.descriptionKey, d.kind)} (${d.value})` : tr(d.descriptionKey, d.kind)
|
|
2128
|
+
}));
|
|
2129
|
+
sections.push({
|
|
2130
|
+
title: tr("editor.visualExplain.diagnostics.title", "Diagnostics"),
|
|
2131
|
+
titleKey: "editor.visualExplain.diagnostics.title",
|
|
2132
|
+
entries: diagEntries
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
return sections;
|
|
2136
|
+
}
|
|
2137
|
+
function formatExplainNodeDetails(node, options = {}) {
|
|
2138
|
+
const sections = getExplainNodeDetailSections(node, options);
|
|
2139
|
+
const parts = [];
|
|
2140
|
+
for (const section of sections) {
|
|
2141
|
+
if (section.entries.length === 0) continue;
|
|
2142
|
+
const lines = section.entries.map((entry) => `${entry.label}: ${entry.value}`);
|
|
2143
|
+
if (options.includeSectionHeaders || section.titleKey !== "editor.visualExplain.general") {
|
|
2144
|
+
parts.push(`${section.title}:
|
|
2145
|
+
${lines.join("\n")}`);
|
|
2146
|
+
} else {
|
|
2147
|
+
parts.push(lines.join("\n"));
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
return parts.join("\n\n");
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/planComparison.ts
|
|
2154
|
+
function checkEngineCompatibility(baselineEngine, currentEngine) {
|
|
2155
|
+
const b = (baselineEngine || "").toLowerCase().trim();
|
|
2156
|
+
const c = (currentEngine || "").toLowerCase().trim();
|
|
2157
|
+
if (b && c && b !== c) {
|
|
2158
|
+
return {
|
|
2159
|
+
compatible: false,
|
|
2160
|
+
reason: `Engines differ (${b} vs ${c}). Explain metrics and costs cannot be compared across different database engines.`
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
return { compatible: true };
|
|
2164
|
+
}
|
|
2165
|
+
function calculateMetricDelta(baselineValue, currentValue) {
|
|
2166
|
+
const b = baselineValue ?? null;
|
|
2167
|
+
const c = currentValue ?? null;
|
|
2168
|
+
if (b == null || c == null) {
|
|
2169
|
+
return {
|
|
2170
|
+
baselineValue: b,
|
|
2171
|
+
currentValue: c,
|
|
2172
|
+
delta: null,
|
|
2173
|
+
ratio: null
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
const delta = c - b;
|
|
2177
|
+
const ratio = b !== 0 ? c / b : null;
|
|
2178
|
+
return {
|
|
2179
|
+
baselineValue: b,
|
|
2180
|
+
currentValue: c,
|
|
2181
|
+
delta,
|
|
2182
|
+
ratio
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
function flattenTree(root, prefix = "0", depth = 0) {
|
|
2186
|
+
const result = [{ node: root, path: prefix, depth }];
|
|
2187
|
+
root.children.forEach((child, i) => {
|
|
2188
|
+
result.push(...flattenTree(child, `${prefix}.${i}`, depth + 1));
|
|
2189
|
+
});
|
|
2190
|
+
return result;
|
|
2191
|
+
}
|
|
2192
|
+
function alignPlanNodes(baselineRoot, currentRoot, baselineMetrics, currentMetrics) {
|
|
2193
|
+
if (!baselineRoot && !currentRoot) return [];
|
|
2194
|
+
if (!baselineRoot && currentRoot) {
|
|
2195
|
+
const currentFlat2 = flattenTree(currentRoot);
|
|
2196
|
+
return currentFlat2.map(({ node }) => {
|
|
2197
|
+
const m = currentMetrics?.get(node.id);
|
|
2198
|
+
return {
|
|
2199
|
+
baselineNodeId: null,
|
|
2200
|
+
currentNodeId: node.id,
|
|
2201
|
+
operatorChanged: false,
|
|
2202
|
+
baselineNodeType: null,
|
|
2203
|
+
currentNodeType: node.node_type,
|
|
2204
|
+
relation: node.relation,
|
|
2205
|
+
status: "added",
|
|
2206
|
+
costDelta: calculateMetricDelta(null, m?.totalCost ?? node.total_cost),
|
|
2207
|
+
timeDelta: calculateMetricDelta(null, m?.inclusiveTimeMs ?? node.actual_time_ms),
|
|
2208
|
+
rowsDelta: calculateMetricDelta(
|
|
2209
|
+
null,
|
|
2210
|
+
m?.totalRows ?? node.actual_rows ?? node.plan_rows
|
|
2211
|
+
)
|
|
2212
|
+
};
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
if (baselineRoot && !currentRoot) {
|
|
2216
|
+
const baselineFlat2 = flattenTree(baselineRoot);
|
|
2217
|
+
return baselineFlat2.map(({ node }) => {
|
|
2218
|
+
const m = baselineMetrics?.get(node.id);
|
|
2219
|
+
return {
|
|
2220
|
+
baselineNodeId: node.id,
|
|
2221
|
+
currentNodeId: null,
|
|
2222
|
+
operatorChanged: false,
|
|
2223
|
+
baselineNodeType: node.node_type,
|
|
2224
|
+
currentNodeType: null,
|
|
2225
|
+
relation: node.relation,
|
|
2226
|
+
status: "removed",
|
|
2227
|
+
costDelta: calculateMetricDelta(m?.totalCost ?? node.total_cost, null),
|
|
2228
|
+
timeDelta: calculateMetricDelta(m?.inclusiveTimeMs ?? node.actual_time_ms, null),
|
|
2229
|
+
rowsDelta: calculateMetricDelta(
|
|
2230
|
+
m?.totalRows ?? node.actual_rows ?? node.plan_rows,
|
|
2231
|
+
null
|
|
2232
|
+
)
|
|
2233
|
+
};
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
const baselineFlat = flattenTree(baselineRoot);
|
|
2237
|
+
const currentFlat = flattenTree(currentRoot);
|
|
2238
|
+
const matchedCurrent = /* @__PURE__ */ new Set();
|
|
2239
|
+
const matchedBaseline = /* @__PURE__ */ new Set();
|
|
2240
|
+
const comparisons = [];
|
|
2241
|
+
const currentByPath = new Map(currentFlat.map((item) => [item.path, item]));
|
|
2242
|
+
for (const bItem of baselineFlat) {
|
|
2243
|
+
const cItem = currentByPath.get(bItem.path);
|
|
2244
|
+
if (cItem && !matchedCurrent.has(cItem.node.id)) {
|
|
2245
|
+
const typeMatch = bItem.node.node_type === cItem.node.node_type;
|
|
2246
|
+
const relMatch = bItem.node.relation === cItem.node.relation;
|
|
2247
|
+
if (typeMatch || bItem.node.relation && relMatch) {
|
|
2248
|
+
matchedBaseline.add(bItem.node.id);
|
|
2249
|
+
matchedCurrent.add(cItem.node.id);
|
|
2250
|
+
const bM = baselineMetrics?.get(bItem.node.id);
|
|
2251
|
+
const cM = currentMetrics?.get(cItem.node.id);
|
|
2252
|
+
comparisons.push({
|
|
2253
|
+
baselineNodeId: bItem.node.id,
|
|
2254
|
+
currentNodeId: cItem.node.id,
|
|
2255
|
+
operatorChanged: !typeMatch,
|
|
2256
|
+
baselineNodeType: bItem.node.node_type,
|
|
2257
|
+
currentNodeType: cItem.node.node_type,
|
|
2258
|
+
relation: cItem.node.relation ?? bItem.node.relation,
|
|
2259
|
+
status: "matched",
|
|
2260
|
+
costDelta: calculateMetricDelta(
|
|
2261
|
+
bM?.totalCost ?? bItem.node.total_cost,
|
|
2262
|
+
cM?.totalCost ?? cItem.node.total_cost
|
|
2263
|
+
),
|
|
2264
|
+
timeDelta: calculateMetricDelta(
|
|
2265
|
+
bM?.inclusiveTimeMs ?? bItem.node.actual_time_ms,
|
|
2266
|
+
cM?.inclusiveTimeMs ?? cItem.node.actual_time_ms
|
|
2267
|
+
),
|
|
2268
|
+
rowsDelta: calculateMetricDelta(
|
|
2269
|
+
bM?.totalRows ?? bItem.node.actual_rows ?? bItem.node.plan_rows,
|
|
2270
|
+
cM?.totalRows ?? cItem.node.actual_rows ?? cItem.node.plan_rows
|
|
2271
|
+
)
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
for (const bItem of baselineFlat) {
|
|
2277
|
+
if (matchedBaseline.has(bItem.node.id)) continue;
|
|
2278
|
+
const candidate = currentFlat.find(
|
|
2279
|
+
(cItem) => !matchedCurrent.has(cItem.node.id) && bItem.node.relation && bItem.node.relation === cItem.node.relation
|
|
2280
|
+
);
|
|
2281
|
+
if (candidate) {
|
|
2282
|
+
matchedBaseline.add(bItem.node.id);
|
|
2283
|
+
matchedCurrent.add(candidate.node.id);
|
|
2284
|
+
const bM = baselineMetrics?.get(bItem.node.id);
|
|
2285
|
+
const cM = currentMetrics?.get(candidate.node.id);
|
|
2286
|
+
const typeMatch = bItem.node.node_type === candidate.node.node_type;
|
|
2287
|
+
comparisons.push({
|
|
2288
|
+
baselineNodeId: bItem.node.id,
|
|
2289
|
+
currentNodeId: candidate.node.id,
|
|
2290
|
+
operatorChanged: !typeMatch,
|
|
2291
|
+
baselineNodeType: bItem.node.node_type,
|
|
2292
|
+
currentNodeType: candidate.node.node_type,
|
|
2293
|
+
relation: candidate.node.relation,
|
|
2294
|
+
status: "matched",
|
|
2295
|
+
isAmbiguous: true,
|
|
2296
|
+
costDelta: calculateMetricDelta(
|
|
2297
|
+
bM?.totalCost ?? bItem.node.total_cost,
|
|
2298
|
+
cM?.totalCost ?? candidate.node.total_cost
|
|
2299
|
+
),
|
|
2300
|
+
timeDelta: calculateMetricDelta(
|
|
2301
|
+
bM?.inclusiveTimeMs ?? bItem.node.actual_time_ms,
|
|
2302
|
+
cM?.inclusiveTimeMs ?? candidate.node.actual_time_ms
|
|
2303
|
+
),
|
|
2304
|
+
rowsDelta: calculateMetricDelta(
|
|
2305
|
+
bM?.totalRows ?? bItem.node.actual_rows ?? bItem.node.plan_rows,
|
|
2306
|
+
cM?.totalRows ?? candidate.node.actual_rows ?? candidate.node.plan_rows
|
|
2307
|
+
)
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
for (const bItem of baselineFlat) {
|
|
2312
|
+
if (!matchedBaseline.has(bItem.node.id)) {
|
|
2313
|
+
const bM = baselineMetrics?.get(bItem.node.id);
|
|
2314
|
+
comparisons.push({
|
|
2315
|
+
baselineNodeId: bItem.node.id,
|
|
2316
|
+
currentNodeId: null,
|
|
2317
|
+
operatorChanged: false,
|
|
2318
|
+
baselineNodeType: bItem.node.node_type,
|
|
2319
|
+
currentNodeType: null,
|
|
2320
|
+
relation: bItem.node.relation,
|
|
2321
|
+
status: "removed",
|
|
2322
|
+
costDelta: calculateMetricDelta(bM?.totalCost ?? bItem.node.total_cost, null),
|
|
2323
|
+
timeDelta: calculateMetricDelta(
|
|
2324
|
+
bM?.inclusiveTimeMs ?? bItem.node.actual_time_ms,
|
|
2325
|
+
null
|
|
2326
|
+
),
|
|
2327
|
+
rowsDelta: calculateMetricDelta(
|
|
2328
|
+
bM?.totalRows ?? bItem.node.actual_rows ?? bItem.node.plan_rows,
|
|
2329
|
+
null
|
|
2330
|
+
)
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
for (const cItem of currentFlat) {
|
|
2335
|
+
if (!matchedCurrent.has(cItem.node.id)) {
|
|
2336
|
+
const cM = currentMetrics?.get(cItem.node.id);
|
|
2337
|
+
comparisons.push({
|
|
2338
|
+
baselineNodeId: null,
|
|
2339
|
+
currentNodeId: cItem.node.id,
|
|
2340
|
+
operatorChanged: false,
|
|
2341
|
+
baselineNodeType: null,
|
|
2342
|
+
currentNodeType: cItem.node.node_type,
|
|
2343
|
+
relation: cItem.node.relation,
|
|
2344
|
+
status: "added",
|
|
2345
|
+
costDelta: calculateMetricDelta(null, cM?.totalCost ?? cItem.node.total_cost),
|
|
2346
|
+
timeDelta: calculateMetricDelta(
|
|
2347
|
+
null,
|
|
2348
|
+
cM?.inclusiveTimeMs ?? cItem.node.actual_time_ms
|
|
2349
|
+
),
|
|
2350
|
+
rowsDelta: calculateMetricDelta(
|
|
2351
|
+
null,
|
|
2352
|
+
cM?.totalRows ?? cItem.node.actual_rows ?? cItem.node.plan_rows
|
|
2353
|
+
)
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return comparisons;
|
|
2358
|
+
}
|
|
2359
|
+
function comparePlanSnapshots(baseline, current, baselineMetrics, currentMetrics) {
|
|
2360
|
+
const engineCheck = checkEngineCompatibility(baseline.context.engine, current.context.engine);
|
|
2361
|
+
const hasAnalyzeMismatch = baseline.hasAnalyzeData !== current.hasAnalyzeData;
|
|
2362
|
+
const nodeComparisons = engineCheck.compatible ? alignPlanNodes(baseline.plan.root, current.plan.root, baselineMetrics, currentMetrics) : [];
|
|
2363
|
+
let nodesAdded = 0;
|
|
2364
|
+
let nodesRemoved = 0;
|
|
2365
|
+
let operatorsChanged = 0;
|
|
2366
|
+
for (const cmp of nodeComparisons) {
|
|
2367
|
+
if (cmp.status === "added") nodesAdded++;
|
|
2368
|
+
if (cmp.status === "removed") nodesRemoved++;
|
|
2369
|
+
if (cmp.status === "matched" && cmp.operatorChanged) operatorsChanged++;
|
|
2370
|
+
}
|
|
2371
|
+
return {
|
|
2372
|
+
isCompatible: engineCheck.compatible,
|
|
2373
|
+
incompatibilityReason: engineCheck.reason,
|
|
2374
|
+
hasAnalyzeMismatch,
|
|
2375
|
+
baselineSnapshotId: baseline.id,
|
|
2376
|
+
currentSnapshotId: current.id,
|
|
2377
|
+
summary: {
|
|
2378
|
+
totalCostDelta: calculateMetricDelta(baseline.totalCost, current.totalCost),
|
|
2379
|
+
executionTimeDelta: calculateMetricDelta(
|
|
2380
|
+
baseline.executionTimeMs,
|
|
2381
|
+
current.executionTimeMs
|
|
2382
|
+
),
|
|
2383
|
+
planningTimeDelta: calculateMetricDelta(
|
|
2384
|
+
baseline.planningTimeMs,
|
|
2385
|
+
current.planningTimeMs
|
|
2386
|
+
),
|
|
2387
|
+
nodesAdded,
|
|
2388
|
+
nodesRemoved,
|
|
2389
|
+
operatorsChanged
|
|
2390
|
+
},
|
|
2391
|
+
nodeComparisons
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
export { EXPLAIN_DIAGNOSTIC_THRESHOLDS, EXPLAIN_METRIC_KINDS, EXPLAIN_TABLE_ANALYZE_COLUMNS, EXPLAIN_TABLE_COLUMNS, NodeIdAllocator, alignPlanNodes, buildSqliteTree, calculateMetricDelta, checkEngineCompatibility, comparePlanSnapshots, computeExplainMetrics, countDiagnosticsBySeverity, detectFormat, detectFormatFor, explainEngineFromDriverName, findExplainNode, flattenExplainNodes, formatCost, formatExplainNodeDetails, formatRatio, formatRows, formatTime, getAvailableMetricKinds, getDefaultMetricKind, getExplainDriverLegend, getExplainNodeDetailSections, getExplainParser, getExplainPlanStats, getExplainPlanSummary, getExplainTableRows, getHeatBarClass, getMaxCost, getMaxTime, getMetricMax, getMetricValue, getNodeCostStyle, getNodeDiagnostics, getNodeMetrics, getPlanDiagnostics, getRowEstimateRatio, getWorstSeverity, hasAnalyzeDataRecursive, isDefaultExplainTableColumnOrder, isMetricAvailable, listExplainParsers, moveExplainTableColumn, normalizeExplainTableColumnOrder, parseExplain, parseExplainFor, parseMysqlJson, parseMysqlTabularRows, parseMysqlText, parsePostgresJson, parsePostgresText, parseRawExplain, parseSqliteEqpRows, registerExplainParser, resolveExplainOutput, shiftExplainTableColumn, unregisterExplainParser, withSourceLabel };
|