@tabularis/explain 0.2.0 → 0.3.1
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 +570 -50
- 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"]),
|
|
@@ -724,7 +726,10 @@ function parsePostgresText(raw) {
|
|
|
724
726
|
let planningTime = null;
|
|
725
727
|
let executionTime = null;
|
|
726
728
|
let hasAnalyzeData = false;
|
|
729
|
+
let hasBuffers = false;
|
|
727
730
|
const ids = new NodeIdAllocator();
|
|
731
|
+
let rootIndent = null;
|
|
732
|
+
let inTrailer = false;
|
|
728
733
|
const stack = [];
|
|
729
734
|
let root = null;
|
|
730
735
|
const promoteOrAttach = (node) => {
|
|
@@ -766,12 +771,16 @@ function parsePostgresText(raw) {
|
|
|
766
771
|
if (node.actual_rows !== null || node.actual_time_ms !== null) {
|
|
767
772
|
hasAnalyzeData = true;
|
|
768
773
|
}
|
|
774
|
+
rootIndent ??= indent;
|
|
775
|
+
inTrailer = false;
|
|
769
776
|
popUntilShallower(indent);
|
|
770
777
|
stack.push({ indent, node });
|
|
771
778
|
} else {
|
|
779
|
+
if (rootIndent !== null && indent <= rootIndent) inTrailer = true;
|
|
780
|
+
if (inTrailer) continue;
|
|
772
781
|
const top = stack[stack.length - 1];
|
|
773
|
-
if (top !== void 0) {
|
|
774
|
-
|
|
782
|
+
if (top !== void 0 && applyTextAttribute(top.node, trimmed)) {
|
|
783
|
+
hasBuffers = true;
|
|
775
784
|
}
|
|
776
785
|
}
|
|
777
786
|
}
|
|
@@ -779,6 +788,7 @@ function parsePostgresText(raw) {
|
|
|
779
788
|
if (root === null) {
|
|
780
789
|
throw new Error("No plan nodes found in EXPLAIN text output");
|
|
781
790
|
}
|
|
791
|
+
if (hasBuffers) fillMissingBuffers(root);
|
|
782
792
|
return {
|
|
783
793
|
root,
|
|
784
794
|
planning_time_ms: planningTime,
|
|
@@ -882,7 +892,7 @@ function parseCostFields(inner) {
|
|
|
882
892
|
function parseActualFields(inner) {
|
|
883
893
|
const trimmed = inner.trim();
|
|
884
894
|
if (trimmed.toLowerCase() === "never executed") {
|
|
885
|
-
return [
|
|
895
|
+
return [0, 0, 0];
|
|
886
896
|
}
|
|
887
897
|
const rest = trimmed.startsWith("actual ") ? trimmed.slice(7) : trimmed;
|
|
888
898
|
let totalTime = null;
|
|
@@ -909,6 +919,10 @@ function parseFiniteNumber(text) {
|
|
|
909
919
|
return Number.isFinite(value) ? value : null;
|
|
910
920
|
}
|
|
911
921
|
function applyTextAttribute(node, content) {
|
|
922
|
+
if (content.startsWith("Buffers:")) {
|
|
923
|
+
applyBuffers(node, content.slice("Buffers:".length));
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
912
926
|
if (content.startsWith("Filter:")) {
|
|
913
927
|
node.filter = content.slice("Filter:".length).trim();
|
|
914
928
|
} else if (content.startsWith("Index Cond:")) {
|
|
@@ -923,6 +937,32 @@ function applyTextAttribute(node, content) {
|
|
|
923
937
|
node.extra[content.slice(0, sep).trim()] = content.slice(sep + 1).trim();
|
|
924
938
|
}
|
|
925
939
|
}
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
function applyBuffers(node, content) {
|
|
943
|
+
node.buffers_hit = 0;
|
|
944
|
+
node.buffers_read = 0;
|
|
945
|
+
for (const group of content.split(",")) {
|
|
946
|
+
const [scope, ...counters] = group.trim().split(/\s+/);
|
|
947
|
+
if (scope === void 0 || scope === "") continue;
|
|
948
|
+
const scopeLabel = scope.charAt(0).toUpperCase() + scope.slice(1).toLowerCase();
|
|
949
|
+
for (const counter of counters) {
|
|
950
|
+
const eq = counter.indexOf("=");
|
|
951
|
+
if (eq === -1) continue;
|
|
952
|
+
const name = counter.slice(0, eq).toLowerCase();
|
|
953
|
+
const value = parseFiniteNumber(counter.slice(eq + 1));
|
|
954
|
+
if (value === null) continue;
|
|
955
|
+
if (scopeLabel === "Shared" && name === "hit") node.buffers_hit = value;
|
|
956
|
+
else if (scopeLabel === "Shared" && name === "read") node.buffers_read = value;
|
|
957
|
+
else
|
|
958
|
+
node.extra[`${scopeLabel} ${name.charAt(0).toUpperCase()}${name.slice(1)} Blocks`] = value;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function fillMissingBuffers(node) {
|
|
963
|
+
node.buffers_hit ??= 0;
|
|
964
|
+
node.buffers_read ??= 0;
|
|
965
|
+
for (const child of node.children) fillMissingBuffers(child);
|
|
926
966
|
}
|
|
927
967
|
|
|
928
968
|
// src/parsers/sqlite.ts
|
|
@@ -1053,16 +1093,12 @@ var BUILTIN_EXPLAIN_PARSERS = Object.freeze([
|
|
|
1053
1093
|
Object.freeze({
|
|
1054
1094
|
engine: "mysql",
|
|
1055
1095
|
format: "mysql-tabular-rows",
|
|
1056
|
-
parse: (payload) => parseMysqlTabularRows(
|
|
1057
|
-
parseJsonRows(payload, "mysql-tabular-rows")
|
|
1058
|
-
)
|
|
1096
|
+
parse: (payload) => parseMysqlTabularRows(parseJsonRows(payload, "mysql-tabular-rows"))
|
|
1059
1097
|
}),
|
|
1060
1098
|
Object.freeze({
|
|
1061
1099
|
engine: "sqlite",
|
|
1062
1100
|
format: "sqlite-eqp-rows",
|
|
1063
|
-
parse: (payload) => parseSqliteEqpRows(
|
|
1064
|
-
parseJsonRows(payload, "sqlite-eqp-rows")
|
|
1065
|
-
)
|
|
1101
|
+
parse: (payload) => parseSqliteEqpRows(parseJsonRows(payload, "sqlite-eqp-rows"))
|
|
1066
1102
|
})
|
|
1067
1103
|
]);
|
|
1068
1104
|
|
|
@@ -1144,19 +1180,19 @@ function explainEngineFromDriverName(name) {
|
|
|
1144
1180
|
);
|
|
1145
1181
|
return parser?.engine ?? null;
|
|
1146
1182
|
}
|
|
1147
|
-
function detectFormat(raw) {
|
|
1148
|
-
return detectFormatFor(raw, null);
|
|
1183
|
+
async function detectFormat(raw) {
|
|
1184
|
+
return await detectFormatFor(raw, null);
|
|
1149
1185
|
}
|
|
1150
|
-
function detectFormatFor(raw, engine) {
|
|
1186
|
+
async function detectFormatFor(raw, engine) {
|
|
1151
1187
|
switch (engine) {
|
|
1152
1188
|
case "postgres":
|
|
1153
|
-
return detectPostgresFormat(raw);
|
|
1189
|
+
return Promise.resolve(detectPostgresFormat(raw));
|
|
1154
1190
|
case "mysql":
|
|
1155
|
-
if (looksLikeJson(raw)) return "mysql-json";
|
|
1191
|
+
if (looksLikeJson(raw)) return Promise.resolve("mysql-json");
|
|
1156
1192
|
if (raw.trim() === "") {
|
|
1157
1193
|
throw new Error("Unsupported EXPLAIN file format: input is empty");
|
|
1158
1194
|
}
|
|
1159
|
-
return "mysql-text";
|
|
1195
|
+
return Promise.resolve("mysql-text");
|
|
1160
1196
|
case "sqlite":
|
|
1161
1197
|
throw new Error(
|
|
1162
1198
|
"SQLite EXPLAIN QUERY PLAN has no text form here: pass its (id, parent, detail) rows to buildSqliteTree"
|
|
@@ -1164,14 +1200,14 @@ function detectFormatFor(raw, engine) {
|
|
|
1164
1200
|
case null: {
|
|
1165
1201
|
const builtinFormat = detectPostgresFormatOrNull(raw);
|
|
1166
1202
|
if (builtinFormat !== null) return builtinFormat;
|
|
1167
|
-
const customFormat = sniffRegisteredFormat(raw, null);
|
|
1203
|
+
const customFormat = await sniffRegisteredFormat(raw, null);
|
|
1168
1204
|
if (customFormat !== null) return customFormat;
|
|
1169
1205
|
throw new Error(
|
|
1170
1206
|
"Unsupported EXPLAIN file format: expected Postgres JSON or text output"
|
|
1171
1207
|
);
|
|
1172
1208
|
}
|
|
1173
1209
|
default: {
|
|
1174
|
-
const format = sniffRegisteredFormat(raw, engine);
|
|
1210
|
+
const format = await sniffRegisteredFormat(raw, engine);
|
|
1175
1211
|
if (format !== null) return format;
|
|
1176
1212
|
throw new Error(`Unsupported EXPLAIN file format for engine '${engine}'`);
|
|
1177
1213
|
}
|
|
@@ -1180,16 +1216,14 @@ function detectFormatFor(raw, engine) {
|
|
|
1180
1216
|
function detectPostgresFormat(raw) {
|
|
1181
1217
|
const format = detectPostgresFormatOrNull(raw);
|
|
1182
1218
|
if (format !== null) return format;
|
|
1183
|
-
throw new Error(
|
|
1184
|
-
"Unsupported EXPLAIN file format: expected Postgres JSON or text output"
|
|
1185
|
-
);
|
|
1219
|
+
throw new Error("Unsupported EXPLAIN file format: expected Postgres JSON or text output");
|
|
1186
1220
|
}
|
|
1187
1221
|
function detectPostgresFormatOrNull(raw) {
|
|
1188
1222
|
if (looksLikeJson(raw)) return "postgres-json";
|
|
1189
1223
|
if (looksLikePostgresText(raw)) return "postgres-text";
|
|
1190
1224
|
return null;
|
|
1191
1225
|
}
|
|
1192
|
-
function sniffRegisteredFormat(raw, engine) {
|
|
1226
|
+
async function sniffRegisteredFormat(raw, engine) {
|
|
1193
1227
|
const normalizedEngine = engine?.trim().toLowerCase() ?? null;
|
|
1194
1228
|
for (const parser of listExplainParsers()) {
|
|
1195
1229
|
if (normalizedEngine === null && BUILTIN_SOURCE_FORMATS.has(parser.format)) {
|
|
@@ -1200,7 +1234,7 @@ function sniffRegisteredFormat(raw, engine) {
|
|
|
1200
1234
|
}
|
|
1201
1235
|
if (parser.sniff === void 0) continue;
|
|
1202
1236
|
try {
|
|
1203
|
-
if (parser.sniff(raw)) return parser.format;
|
|
1237
|
+
if (await parser.sniff(raw)) return parser.format;
|
|
1204
1238
|
} catch {
|
|
1205
1239
|
}
|
|
1206
1240
|
}
|
|
@@ -1216,8 +1250,9 @@ function looksLikePostgresText(raw) {
|
|
|
1216
1250
|
function parseExplain(raw) {
|
|
1217
1251
|
return parseExplainFor(raw, null);
|
|
1218
1252
|
}
|
|
1219
|
-
function parseExplainFor(raw, engine) {
|
|
1220
|
-
|
|
1253
|
+
async function parseExplainFor(raw, engine) {
|
|
1254
|
+
const format = await detectFormatFor(raw, engine);
|
|
1255
|
+
return await parserFor(format).parse(raw);
|
|
1221
1256
|
}
|
|
1222
1257
|
function withSourceLabel(plan, name) {
|
|
1223
1258
|
if (plan.original_query === "") {
|
|
@@ -1227,14 +1262,14 @@ function withSourceLabel(plan, name) {
|
|
|
1227
1262
|
}
|
|
1228
1263
|
|
|
1229
1264
|
// src/raw.ts
|
|
1230
|
-
function parseRawExplain(raw) {
|
|
1265
|
+
async function parseRawExplain(raw) {
|
|
1231
1266
|
const parser = getExplainParser(raw.format);
|
|
1232
1267
|
if (parser === null) {
|
|
1233
1268
|
throw new Error(
|
|
1234
1269
|
`No EXPLAIN parser registered for format '${raw.format}' (engine '${raw.engine}'). Import the parser package for '${raw.engine}' before parsing.`
|
|
1235
1270
|
);
|
|
1236
1271
|
}
|
|
1237
|
-
const plan = parser.parse(raw.payload);
|
|
1272
|
+
const plan = await parser.parse(raw.payload);
|
|
1238
1273
|
return {
|
|
1239
1274
|
...plan,
|
|
1240
1275
|
driver: raw.engine,
|
|
@@ -1242,7 +1277,7 @@ function parseRawExplain(raw) {
|
|
|
1242
1277
|
};
|
|
1243
1278
|
}
|
|
1244
1279
|
function resolveExplainOutput(output) {
|
|
1245
|
-
return output.kind === "plan" ? output.plan : parseRawExplain(output.raw);
|
|
1280
|
+
return output.kind === "plan" ? Promise.resolve(output.plan) : parseRawExplain(output.raw);
|
|
1246
1281
|
}
|
|
1247
1282
|
|
|
1248
1283
|
// src/metrics.ts
|
|
@@ -1323,16 +1358,10 @@ function computeExplainMetrics(plan) {
|
|
|
1323
1358
|
let maxExclusiveBuffers = 0;
|
|
1324
1359
|
let totalExclusiveTimeMs = 0;
|
|
1325
1360
|
for (const metrics of order) {
|
|
1326
|
-
maxExclusiveTimeMs = Math.max(
|
|
1327
|
-
maxExclusiveTimeMs,
|
|
1328
|
-
metrics.exclusiveTimeMs ?? 0
|
|
1329
|
-
);
|
|
1361
|
+
maxExclusiveTimeMs = Math.max(maxExclusiveTimeMs, metrics.exclusiveTimeMs ?? 0);
|
|
1330
1362
|
maxExclusiveCost = Math.max(maxExclusiveCost, metrics.exclusiveCost ?? 0);
|
|
1331
1363
|
maxTotalRows = Math.max(maxTotalRows, metrics.totalRows ?? 0);
|
|
1332
|
-
maxExclusiveBuffers = Math.max(
|
|
1333
|
-
maxExclusiveBuffers,
|
|
1334
|
-
metrics.exclusiveBuffers ?? 0
|
|
1335
|
-
);
|
|
1364
|
+
maxExclusiveBuffers = Math.max(maxExclusiveBuffers, metrics.exclusiveBuffers ?? 0);
|
|
1336
1365
|
totalExclusiveTimeMs += metrics.exclusiveTimeMs ?? 0;
|
|
1337
1366
|
}
|
|
1338
1367
|
if (totalExclusiveTimeMs > 0) {
|
|
@@ -1481,7 +1510,7 @@ function getRowEstimateRatio(node) {
|
|
|
1481
1510
|
}
|
|
1482
1511
|
return node.actual_rows / node.plan_rows;
|
|
1483
1512
|
}
|
|
1484
|
-
function
|
|
1513
|
+
function getRowEstimateGap(node) {
|
|
1485
1514
|
const ratio = getRowEstimateRatio(node);
|
|
1486
1515
|
if (ratio == null) {
|
|
1487
1516
|
return null;
|
|
@@ -1527,7 +1556,7 @@ function getExplainPlanSummary(plan, metrics) {
|
|
|
1527
1556
|
};
|
|
1528
1557
|
}
|
|
1529
1558
|
const ratio = getRowEstimateRatio(node);
|
|
1530
|
-
const magnitude =
|
|
1559
|
+
const magnitude = getRowEstimateGap(node);
|
|
1531
1560
|
if (ratio != null && magnitude != null && (largestRowMismatchNode == null || magnitude > largestRowMismatchNode.value)) {
|
|
1532
1561
|
largestRowMismatchNode = {
|
|
1533
1562
|
nodeId: node.id,
|
|
@@ -1571,14 +1600,33 @@ function getExplainDriverLegend(plan) {
|
|
|
1571
1600
|
"editor.visualExplain.mysqlEstimateLegend2"
|
|
1572
1601
|
];
|
|
1573
1602
|
case "sqlite":
|
|
1574
|
-
return [
|
|
1575
|
-
"editor.visualExplain.sqliteLegend1",
|
|
1576
|
-
"editor.visualExplain.sqliteLegend2"
|
|
1577
|
-
];
|
|
1603
|
+
return ["editor.visualExplain.sqliteLegend1", "editor.visualExplain.sqliteLegend2"];
|
|
1578
1604
|
default:
|
|
1579
1605
|
return [];
|
|
1580
1606
|
}
|
|
1581
1607
|
}
|
|
1608
|
+
function getExplainTableRow(node, hasAnalyzeData, nodeMetrics, fallbackIndex = 1) {
|
|
1609
|
+
return {
|
|
1610
|
+
index: nodeMetrics?.index ?? fallbackIndex,
|
|
1611
|
+
depth: nodeMetrics?.depth ?? 0,
|
|
1612
|
+
nodeType: node.node_type,
|
|
1613
|
+
relation: node.relation ?? null,
|
|
1614
|
+
startupCost: node.startup_cost ?? null,
|
|
1615
|
+
totalCost: node.total_cost ?? null,
|
|
1616
|
+
planRows: node.plan_rows ?? null,
|
|
1617
|
+
actualRows: hasAnalyzeData ? node.actual_rows ?? null : null,
|
|
1618
|
+
actualLoops: hasAnalyzeData ? node.actual_loops ?? null : null,
|
|
1619
|
+
exclusiveTimeMs: hasAnalyzeData ? nodeMetrics?.exclusiveTimeMs ?? null : null,
|
|
1620
|
+
estimateGap: getRowEstimateGap(node),
|
|
1621
|
+
filter: node.filter ?? null
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
function getExplainTableRows(plan, metrics) {
|
|
1625
|
+
const planMetrics = metrics ?? computeExplainMetrics(plan);
|
|
1626
|
+
return flattenExplainNodes(plan.root).map(
|
|
1627
|
+
(node, i) => getExplainTableRow(node, plan.has_analyze_data, planMetrics.byId.get(node.id), i + 1)
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1582
1630
|
|
|
1583
1631
|
// src/diagnostics.ts
|
|
1584
1632
|
var EXPLAIN_DIAGNOSTIC_THRESHOLDS = {
|
|
@@ -1764,9 +1812,7 @@ function getNodeDiagnostics(node, metrics) {
|
|
|
1764
1812
|
value: formatRows(node.buffers_read)
|
|
1765
1813
|
});
|
|
1766
1814
|
}
|
|
1767
|
-
return diagnostics.sort(
|
|
1768
|
-
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
|
|
1769
|
-
);
|
|
1815
|
+
return diagnostics.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
1770
1816
|
}
|
|
1771
1817
|
function getPlanDiagnostics(plan, metrics) {
|
|
1772
1818
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -1910,4 +1956,478 @@ function compareByTimeThenCount(a, b) {
|
|
|
1910
1956
|
return (b.exclusiveTimeMs ?? 0) - (a.exclusiveTimeMs ?? 0) || b.count - a.count || a.nodeType.localeCompare(b.nodeType);
|
|
1911
1957
|
}
|
|
1912
1958
|
|
|
1913
|
-
|
|
1959
|
+
// src/tableColumns.ts
|
|
1960
|
+
var EXPLAIN_TABLE_COLUMNS = [
|
|
1961
|
+
"operation",
|
|
1962
|
+
"relation",
|
|
1963
|
+
"cost",
|
|
1964
|
+
"estRows",
|
|
1965
|
+
"actualRows",
|
|
1966
|
+
"loops",
|
|
1967
|
+
"selfTime",
|
|
1968
|
+
"estimateGap",
|
|
1969
|
+
"filter"
|
|
1970
|
+
];
|
|
1971
|
+
var EXPLAIN_TABLE_ANALYZE_COLUMNS = /* @__PURE__ */ new Set([
|
|
1972
|
+
"actualRows",
|
|
1973
|
+
"loops"
|
|
1974
|
+
]);
|
|
1975
|
+
function isExplainTableColumnId(value) {
|
|
1976
|
+
return typeof value === "string" && EXPLAIN_TABLE_COLUMNS.includes(value);
|
|
1977
|
+
}
|
|
1978
|
+
function normalizeExplainTableColumnOrder(saved) {
|
|
1979
|
+
const order = [];
|
|
1980
|
+
if (Array.isArray(saved)) {
|
|
1981
|
+
for (const id of saved) {
|
|
1982
|
+
if (isExplainTableColumnId(id) && !order.includes(id)) order.push(id);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
EXPLAIN_TABLE_COLUMNS.forEach((id, defaultIndex) => {
|
|
1986
|
+
if (order.includes(id)) return;
|
|
1987
|
+
const previous = EXPLAIN_TABLE_COLUMNS.slice(0, defaultIndex).reverse();
|
|
1988
|
+
const anchor = previous.find((candidate) => order.includes(candidate));
|
|
1989
|
+
order.splice(anchor ? order.indexOf(anchor) + 1 : 0, 0, id);
|
|
1990
|
+
});
|
|
1991
|
+
return order;
|
|
1992
|
+
}
|
|
1993
|
+
function moveExplainTableColumn(order, column, target) {
|
|
1994
|
+
const from = order.indexOf(column);
|
|
1995
|
+
const to = order.indexOf(target);
|
|
1996
|
+
if (from === -1 || to === -1 || from === to) return order;
|
|
1997
|
+
const next = order.filter((id) => id !== column);
|
|
1998
|
+
next.splice(to, 0, column);
|
|
1999
|
+
return next;
|
|
2000
|
+
}
|
|
2001
|
+
function shiftExplainTableColumn(order, column, direction, visible = order) {
|
|
2002
|
+
const shown = order.filter((id) => visible.includes(id));
|
|
2003
|
+
const at = shown.indexOf(column);
|
|
2004
|
+
const neighbour = at === -1 ? void 0 : shown[at + direction];
|
|
2005
|
+
return neighbour ? moveExplainTableColumn(order, column, neighbour) : order;
|
|
2006
|
+
}
|
|
2007
|
+
function isDefaultExplainTableColumnOrder(order) {
|
|
2008
|
+
return order.length === EXPLAIN_TABLE_COLUMNS.length && order.every((id, index) => id === EXPLAIN_TABLE_COLUMNS[index]);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// src/nodeDetails.ts
|
|
2012
|
+
var DEFAULT_LABELS = {
|
|
2013
|
+
"editor.visualExplain.nodeType": "Operation",
|
|
2014
|
+
"editor.visualExplain.relation": "Table",
|
|
2015
|
+
"editor.visualExplain.cost": "Cost",
|
|
2016
|
+
"editor.visualExplain.selfCost": "Self Cost",
|
|
2017
|
+
"editor.visualExplain.estRows": "Est. Rows",
|
|
2018
|
+
"editor.visualExplain.filter": "Filter",
|
|
2019
|
+
"editor.visualExplain.indexCondition": "Index Cond.",
|
|
2020
|
+
"editor.visualExplain.joinType": "Join Type",
|
|
2021
|
+
"editor.visualExplain.hashCondition": "Hash Cond.",
|
|
2022
|
+
"editor.visualExplain.actualRows": "Actual Rows",
|
|
2023
|
+
"editor.visualExplain.rowsAllLoops": "Rows (All Loops)",
|
|
2024
|
+
"editor.visualExplain.selfTime": "Self Time",
|
|
2025
|
+
"editor.visualExplain.totalTime": "Total Time",
|
|
2026
|
+
"editor.visualExplain.timePerLoop": "Time per Loop",
|
|
2027
|
+
"editor.visualExplain.loops": "Loops",
|
|
2028
|
+
"editor.visualExplain.buffersHit": "Buffers Hit",
|
|
2029
|
+
"editor.visualExplain.buffersRead": "Buffers Read",
|
|
2030
|
+
"editor.visualExplain.general": "General",
|
|
2031
|
+
"editor.visualExplain.analyzeData": "Analyze Data",
|
|
2032
|
+
"editor.visualExplain.extraDetails": "Extra",
|
|
2033
|
+
"editor.visualExplain.diagnostics.title": "Diagnostics"
|
|
2034
|
+
};
|
|
2035
|
+
function getExplainNodeDetailSections(node, options = {}) {
|
|
2036
|
+
const { hasAnalyzeData = false, metrics = null, diagnostics = [], t } = options;
|
|
2037
|
+
const tr = (key, fallback) => {
|
|
2038
|
+
if (t) {
|
|
2039
|
+
return t(key, fallback ?? DEFAULT_LABELS[key] ?? key);
|
|
2040
|
+
}
|
|
2041
|
+
return fallback ?? DEFAULT_LABELS[key] ?? key;
|
|
2042
|
+
};
|
|
2043
|
+
const generalEntries = [
|
|
2044
|
+
{ label: tr("editor.visualExplain.nodeType", "Operation"), value: node.node_type },
|
|
2045
|
+
...node.relation ? [{ label: tr("editor.visualExplain.relation", "Table"), value: node.relation }] : [],
|
|
2046
|
+
...node.startup_cost != null && node.total_cost != null ? [
|
|
2047
|
+
{
|
|
2048
|
+
label: tr("editor.visualExplain.cost", "Cost"),
|
|
2049
|
+
value: `${formatCost(node.startup_cost)} - ${formatCost(node.total_cost)}`
|
|
2050
|
+
}
|
|
2051
|
+
] : node.total_cost != null ? [
|
|
2052
|
+
{
|
|
2053
|
+
label: tr("editor.visualExplain.cost", "Cost"),
|
|
2054
|
+
value: formatCost(node.total_cost)
|
|
2055
|
+
}
|
|
2056
|
+
] : [],
|
|
2057
|
+
...metrics?.exclusiveCost != null ? [
|
|
2058
|
+
{
|
|
2059
|
+
label: tr("editor.visualExplain.selfCost", "Self Cost"),
|
|
2060
|
+
value: formatCost(metrics.exclusiveCost)
|
|
2061
|
+
}
|
|
2062
|
+
] : [],
|
|
2063
|
+
...node.plan_rows != null ? [
|
|
2064
|
+
{
|
|
2065
|
+
label: tr("editor.visualExplain.estRows", "Est. Rows"),
|
|
2066
|
+
value: formatRows(node.plan_rows)
|
|
2067
|
+
}
|
|
2068
|
+
] : [],
|
|
2069
|
+
...node.filter ? [{ label: tr("editor.visualExplain.filter", "Filter"), value: node.filter }] : [],
|
|
2070
|
+
...node.index_condition ? [
|
|
2071
|
+
{
|
|
2072
|
+
label: tr("editor.visualExplain.indexCondition", "Index Cond."),
|
|
2073
|
+
value: node.index_condition
|
|
2074
|
+
}
|
|
2075
|
+
] : [],
|
|
2076
|
+
...node.join_type ? [{ label: tr("editor.visualExplain.joinType", "Join Type"), value: node.join_type }] : [],
|
|
2077
|
+
...node.hash_condition ? [
|
|
2078
|
+
{
|
|
2079
|
+
label: tr("editor.visualExplain.hashCondition", "Hash Cond."),
|
|
2080
|
+
value: node.hash_condition
|
|
2081
|
+
}
|
|
2082
|
+
] : []
|
|
2083
|
+
];
|
|
2084
|
+
const analyzeEntries = hasAnalyzeData ? [
|
|
2085
|
+
...node.actual_rows != null ? [
|
|
2086
|
+
{
|
|
2087
|
+
label: tr("editor.visualExplain.actualRows", "Actual Rows"),
|
|
2088
|
+
value: formatRows(node.actual_rows)
|
|
2089
|
+
}
|
|
2090
|
+
] : [],
|
|
2091
|
+
...metrics?.totalRows != null && node.actual_loops != null && node.actual_loops > 1 ? [
|
|
2092
|
+
{
|
|
2093
|
+
label: tr("editor.visualExplain.rowsAllLoops", "Rows (All Loops)"),
|
|
2094
|
+
value: formatRows(metrics.totalRows)
|
|
2095
|
+
}
|
|
2096
|
+
] : [],
|
|
2097
|
+
...metrics?.exclusiveTimeMs != null ? [
|
|
2098
|
+
{
|
|
2099
|
+
label: tr("editor.visualExplain.selfTime", "Self Time"),
|
|
2100
|
+
value: metrics.timeShare != null ? `${formatTime(metrics.exclusiveTimeMs)} (${Math.round(metrics.timeShare * 100)}%)` : formatTime(metrics.exclusiveTimeMs)
|
|
2101
|
+
}
|
|
2102
|
+
] : [],
|
|
2103
|
+
...metrics?.inclusiveTimeMs != null ? [
|
|
2104
|
+
{
|
|
2105
|
+
label: tr("editor.visualExplain.totalTime", "Total Time"),
|
|
2106
|
+
value: formatTime(metrics.inclusiveTimeMs)
|
|
2107
|
+
}
|
|
2108
|
+
] : [],
|
|
2109
|
+
...node.actual_time_ms != null ? [
|
|
2110
|
+
{
|
|
2111
|
+
label: tr("editor.visualExplain.timePerLoop", "Time per Loop"),
|
|
2112
|
+
value: formatTime(node.actual_time_ms)
|
|
2113
|
+
}
|
|
2114
|
+
] : [],
|
|
2115
|
+
...node.actual_loops != null ? [
|
|
2116
|
+
{
|
|
2117
|
+
label: tr("editor.visualExplain.loops", "Loops"),
|
|
2118
|
+
value: String(node.actual_loops)
|
|
2119
|
+
}
|
|
2120
|
+
] : [],
|
|
2121
|
+
...node.buffers_hit != null ? [
|
|
2122
|
+
{
|
|
2123
|
+
label: tr("editor.visualExplain.buffersHit", "Buffers Hit"),
|
|
2124
|
+
value: String(node.buffers_hit)
|
|
2125
|
+
}
|
|
2126
|
+
] : [],
|
|
2127
|
+
...node.buffers_read != null ? [
|
|
2128
|
+
{
|
|
2129
|
+
label: tr("editor.visualExplain.buffersRead", "Buffers Read"),
|
|
2130
|
+
value: String(node.buffers_read)
|
|
2131
|
+
}
|
|
2132
|
+
] : []
|
|
2133
|
+
] : [];
|
|
2134
|
+
const extraEntries = Object.entries(node.extra ?? {}).map(
|
|
2135
|
+
([key, value]) => ({
|
|
2136
|
+
label: key,
|
|
2137
|
+
value: typeof value === "string" ? value : JSON.stringify(value)
|
|
2138
|
+
})
|
|
2139
|
+
);
|
|
2140
|
+
const sections = [];
|
|
2141
|
+
if (generalEntries.length > 0) {
|
|
2142
|
+
sections.push({
|
|
2143
|
+
title: tr("editor.visualExplain.general", "General"),
|
|
2144
|
+
titleKey: "editor.visualExplain.general",
|
|
2145
|
+
entries: generalEntries
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
if (analyzeEntries.length > 0) {
|
|
2149
|
+
sections.push({
|
|
2150
|
+
title: tr("editor.visualExplain.analyzeData", "Analyze Data"),
|
|
2151
|
+
titleKey: "editor.visualExplain.analyzeData",
|
|
2152
|
+
entries: analyzeEntries
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
if (extraEntries.length > 0) {
|
|
2156
|
+
sections.push({
|
|
2157
|
+
title: tr("editor.visualExplain.extraDetails", "Extra"),
|
|
2158
|
+
titleKey: "editor.visualExplain.extraDetails",
|
|
2159
|
+
entries: extraEntries
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
if (diagnostics && diagnostics.length > 0) {
|
|
2163
|
+
const diagEntries = diagnostics.map((d) => ({
|
|
2164
|
+
label: tr(d.labelKey, d.kind),
|
|
2165
|
+
value: d.value ? `${tr(d.descriptionKey, d.kind)} (${d.value})` : tr(d.descriptionKey, d.kind)
|
|
2166
|
+
}));
|
|
2167
|
+
sections.push({
|
|
2168
|
+
title: tr("editor.visualExplain.diagnostics.title", "Diagnostics"),
|
|
2169
|
+
titleKey: "editor.visualExplain.diagnostics.title",
|
|
2170
|
+
entries: diagEntries
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
return sections;
|
|
2174
|
+
}
|
|
2175
|
+
function formatExplainNodeDetails(node, options = {}) {
|
|
2176
|
+
const sections = getExplainNodeDetailSections(node, options);
|
|
2177
|
+
const parts = [];
|
|
2178
|
+
for (const section of sections) {
|
|
2179
|
+
if (section.entries.length === 0) continue;
|
|
2180
|
+
const lines = section.entries.map((entry) => `${entry.label}: ${entry.value}`);
|
|
2181
|
+
if (options.includeSectionHeaders || section.titleKey !== "editor.visualExplain.general") {
|
|
2182
|
+
parts.push(`${section.title}:
|
|
2183
|
+
${lines.join("\n")}`);
|
|
2184
|
+
} else {
|
|
2185
|
+
parts.push(lines.join("\n"));
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return parts.join("\n\n");
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/planComparison.ts
|
|
2192
|
+
function checkEngineCompatibility(baselineEngine, currentEngine) {
|
|
2193
|
+
const b = (baselineEngine || "").toLowerCase().trim();
|
|
2194
|
+
const c = (currentEngine || "").toLowerCase().trim();
|
|
2195
|
+
if (b && c && b !== c) {
|
|
2196
|
+
return {
|
|
2197
|
+
compatible: false,
|
|
2198
|
+
reason: `Engines differ (${b} vs ${c}). Explain metrics and costs cannot be compared across different database engines.`
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
return { compatible: true };
|
|
2202
|
+
}
|
|
2203
|
+
function calculateMetricDelta(baselineValue, currentValue) {
|
|
2204
|
+
const b = baselineValue ?? null;
|
|
2205
|
+
const c = currentValue ?? null;
|
|
2206
|
+
if (b == null || c == null) {
|
|
2207
|
+
return {
|
|
2208
|
+
baselineValue: b,
|
|
2209
|
+
currentValue: c,
|
|
2210
|
+
delta: null,
|
|
2211
|
+
ratio: null
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
const delta = c - b;
|
|
2215
|
+
const ratio = b !== 0 ? c / b : null;
|
|
2216
|
+
return {
|
|
2217
|
+
baselineValue: b,
|
|
2218
|
+
currentValue: c,
|
|
2219
|
+
delta,
|
|
2220
|
+
ratio
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
function flattenTree(root, prefix = "0", depth = 0) {
|
|
2224
|
+
const result = [{ node: root, path: prefix, depth }];
|
|
2225
|
+
root.children.forEach((child, i) => {
|
|
2226
|
+
result.push(...flattenTree(child, `${prefix}.${i}`, depth + 1));
|
|
2227
|
+
});
|
|
2228
|
+
return result;
|
|
2229
|
+
}
|
|
2230
|
+
function alignPlanNodes(baselineRoot, currentRoot, baselineMetrics, currentMetrics) {
|
|
2231
|
+
if (!baselineRoot && !currentRoot) return [];
|
|
2232
|
+
if (!baselineRoot && currentRoot) {
|
|
2233
|
+
const currentFlat2 = flattenTree(currentRoot);
|
|
2234
|
+
return currentFlat2.map(({ node }) => {
|
|
2235
|
+
const m = currentMetrics?.get(node.id);
|
|
2236
|
+
return {
|
|
2237
|
+
baselineNodeId: null,
|
|
2238
|
+
currentNodeId: node.id,
|
|
2239
|
+
operatorChanged: false,
|
|
2240
|
+
baselineNodeType: null,
|
|
2241
|
+
currentNodeType: node.node_type,
|
|
2242
|
+
relation: node.relation,
|
|
2243
|
+
status: "added",
|
|
2244
|
+
costDelta: calculateMetricDelta(null, m?.totalCost ?? node.total_cost),
|
|
2245
|
+
timeDelta: calculateMetricDelta(null, m?.inclusiveTimeMs ?? node.actual_time_ms),
|
|
2246
|
+
rowsDelta: calculateMetricDelta(
|
|
2247
|
+
null,
|
|
2248
|
+
m?.totalRows ?? node.actual_rows ?? node.plan_rows
|
|
2249
|
+
)
|
|
2250
|
+
};
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
if (baselineRoot && !currentRoot) {
|
|
2254
|
+
const baselineFlat2 = flattenTree(baselineRoot);
|
|
2255
|
+
return baselineFlat2.map(({ node }) => {
|
|
2256
|
+
const m = baselineMetrics?.get(node.id);
|
|
2257
|
+
return {
|
|
2258
|
+
baselineNodeId: node.id,
|
|
2259
|
+
currentNodeId: null,
|
|
2260
|
+
operatorChanged: false,
|
|
2261
|
+
baselineNodeType: node.node_type,
|
|
2262
|
+
currentNodeType: null,
|
|
2263
|
+
relation: node.relation,
|
|
2264
|
+
status: "removed",
|
|
2265
|
+
costDelta: calculateMetricDelta(m?.totalCost ?? node.total_cost, null),
|
|
2266
|
+
timeDelta: calculateMetricDelta(m?.inclusiveTimeMs ?? node.actual_time_ms, null),
|
|
2267
|
+
rowsDelta: calculateMetricDelta(
|
|
2268
|
+
m?.totalRows ?? node.actual_rows ?? node.plan_rows,
|
|
2269
|
+
null
|
|
2270
|
+
)
|
|
2271
|
+
};
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
const baselineFlat = flattenTree(baselineRoot);
|
|
2275
|
+
const currentFlat = flattenTree(currentRoot);
|
|
2276
|
+
const matchedCurrent = /* @__PURE__ */ new Set();
|
|
2277
|
+
const matchedBaseline = /* @__PURE__ */ new Set();
|
|
2278
|
+
const comparisons = [];
|
|
2279
|
+
const currentByPath = new Map(currentFlat.map((item) => [item.path, item]));
|
|
2280
|
+
for (const bItem of baselineFlat) {
|
|
2281
|
+
const cItem = currentByPath.get(bItem.path);
|
|
2282
|
+
if (cItem && !matchedCurrent.has(cItem.node.id)) {
|
|
2283
|
+
const typeMatch = bItem.node.node_type === cItem.node.node_type;
|
|
2284
|
+
const relMatch = bItem.node.relation === cItem.node.relation;
|
|
2285
|
+
if (typeMatch || bItem.node.relation && relMatch) {
|
|
2286
|
+
matchedBaseline.add(bItem.node.id);
|
|
2287
|
+
matchedCurrent.add(cItem.node.id);
|
|
2288
|
+
const bM = baselineMetrics?.get(bItem.node.id);
|
|
2289
|
+
const cM = currentMetrics?.get(cItem.node.id);
|
|
2290
|
+
comparisons.push({
|
|
2291
|
+
baselineNodeId: bItem.node.id,
|
|
2292
|
+
currentNodeId: cItem.node.id,
|
|
2293
|
+
operatorChanged: !typeMatch,
|
|
2294
|
+
baselineNodeType: bItem.node.node_type,
|
|
2295
|
+
currentNodeType: cItem.node.node_type,
|
|
2296
|
+
relation: cItem.node.relation ?? bItem.node.relation,
|
|
2297
|
+
status: "matched",
|
|
2298
|
+
costDelta: calculateMetricDelta(
|
|
2299
|
+
bM?.totalCost ?? bItem.node.total_cost,
|
|
2300
|
+
cM?.totalCost ?? cItem.node.total_cost
|
|
2301
|
+
),
|
|
2302
|
+
timeDelta: calculateMetricDelta(
|
|
2303
|
+
bM?.inclusiveTimeMs ?? bItem.node.actual_time_ms,
|
|
2304
|
+
cM?.inclusiveTimeMs ?? cItem.node.actual_time_ms
|
|
2305
|
+
),
|
|
2306
|
+
rowsDelta: calculateMetricDelta(
|
|
2307
|
+
bM?.totalRows ?? bItem.node.actual_rows ?? bItem.node.plan_rows,
|
|
2308
|
+
cM?.totalRows ?? cItem.node.actual_rows ?? cItem.node.plan_rows
|
|
2309
|
+
)
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
for (const bItem of baselineFlat) {
|
|
2315
|
+
if (matchedBaseline.has(bItem.node.id)) continue;
|
|
2316
|
+
const candidate = currentFlat.find(
|
|
2317
|
+
(cItem) => !matchedCurrent.has(cItem.node.id) && bItem.node.relation && bItem.node.relation === cItem.node.relation
|
|
2318
|
+
);
|
|
2319
|
+
if (candidate) {
|
|
2320
|
+
matchedBaseline.add(bItem.node.id);
|
|
2321
|
+
matchedCurrent.add(candidate.node.id);
|
|
2322
|
+
const bM = baselineMetrics?.get(bItem.node.id);
|
|
2323
|
+
const cM = currentMetrics?.get(candidate.node.id);
|
|
2324
|
+
const typeMatch = bItem.node.node_type === candidate.node.node_type;
|
|
2325
|
+
comparisons.push({
|
|
2326
|
+
baselineNodeId: bItem.node.id,
|
|
2327
|
+
currentNodeId: candidate.node.id,
|
|
2328
|
+
operatorChanged: !typeMatch,
|
|
2329
|
+
baselineNodeType: bItem.node.node_type,
|
|
2330
|
+
currentNodeType: candidate.node.node_type,
|
|
2331
|
+
relation: candidate.node.relation,
|
|
2332
|
+
status: "matched",
|
|
2333
|
+
isAmbiguous: true,
|
|
2334
|
+
costDelta: calculateMetricDelta(
|
|
2335
|
+
bM?.totalCost ?? bItem.node.total_cost,
|
|
2336
|
+
cM?.totalCost ?? candidate.node.total_cost
|
|
2337
|
+
),
|
|
2338
|
+
timeDelta: calculateMetricDelta(
|
|
2339
|
+
bM?.inclusiveTimeMs ?? bItem.node.actual_time_ms,
|
|
2340
|
+
cM?.inclusiveTimeMs ?? candidate.node.actual_time_ms
|
|
2341
|
+
),
|
|
2342
|
+
rowsDelta: calculateMetricDelta(
|
|
2343
|
+
bM?.totalRows ?? bItem.node.actual_rows ?? bItem.node.plan_rows,
|
|
2344
|
+
cM?.totalRows ?? candidate.node.actual_rows ?? candidate.node.plan_rows
|
|
2345
|
+
)
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
for (const bItem of baselineFlat) {
|
|
2350
|
+
if (!matchedBaseline.has(bItem.node.id)) {
|
|
2351
|
+
const bM = baselineMetrics?.get(bItem.node.id);
|
|
2352
|
+
comparisons.push({
|
|
2353
|
+
baselineNodeId: bItem.node.id,
|
|
2354
|
+
currentNodeId: null,
|
|
2355
|
+
operatorChanged: false,
|
|
2356
|
+
baselineNodeType: bItem.node.node_type,
|
|
2357
|
+
currentNodeType: null,
|
|
2358
|
+
relation: bItem.node.relation,
|
|
2359
|
+
status: "removed",
|
|
2360
|
+
costDelta: calculateMetricDelta(bM?.totalCost ?? bItem.node.total_cost, null),
|
|
2361
|
+
timeDelta: calculateMetricDelta(
|
|
2362
|
+
bM?.inclusiveTimeMs ?? bItem.node.actual_time_ms,
|
|
2363
|
+
null
|
|
2364
|
+
),
|
|
2365
|
+
rowsDelta: calculateMetricDelta(
|
|
2366
|
+
bM?.totalRows ?? bItem.node.actual_rows ?? bItem.node.plan_rows,
|
|
2367
|
+
null
|
|
2368
|
+
)
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
for (const cItem of currentFlat) {
|
|
2373
|
+
if (!matchedCurrent.has(cItem.node.id)) {
|
|
2374
|
+
const cM = currentMetrics?.get(cItem.node.id);
|
|
2375
|
+
comparisons.push({
|
|
2376
|
+
baselineNodeId: null,
|
|
2377
|
+
currentNodeId: cItem.node.id,
|
|
2378
|
+
operatorChanged: false,
|
|
2379
|
+
baselineNodeType: null,
|
|
2380
|
+
currentNodeType: cItem.node.node_type,
|
|
2381
|
+
relation: cItem.node.relation,
|
|
2382
|
+
status: "added",
|
|
2383
|
+
costDelta: calculateMetricDelta(null, cM?.totalCost ?? cItem.node.total_cost),
|
|
2384
|
+
timeDelta: calculateMetricDelta(
|
|
2385
|
+
null,
|
|
2386
|
+
cM?.inclusiveTimeMs ?? cItem.node.actual_time_ms
|
|
2387
|
+
),
|
|
2388
|
+
rowsDelta: calculateMetricDelta(
|
|
2389
|
+
null,
|
|
2390
|
+
cM?.totalRows ?? cItem.node.actual_rows ?? cItem.node.plan_rows
|
|
2391
|
+
)
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return comparisons;
|
|
2396
|
+
}
|
|
2397
|
+
function comparePlanSnapshots(baseline, current, baselineMetrics, currentMetrics) {
|
|
2398
|
+
const engineCheck = checkEngineCompatibility(baseline.context.engine, current.context.engine);
|
|
2399
|
+
const hasAnalyzeMismatch = baseline.hasAnalyzeData !== current.hasAnalyzeData;
|
|
2400
|
+
const nodeComparisons = engineCheck.compatible ? alignPlanNodes(baseline.plan.root, current.plan.root, baselineMetrics, currentMetrics) : [];
|
|
2401
|
+
let nodesAdded = 0;
|
|
2402
|
+
let nodesRemoved = 0;
|
|
2403
|
+
let operatorsChanged = 0;
|
|
2404
|
+
for (const cmp of nodeComparisons) {
|
|
2405
|
+
if (cmp.status === "added") nodesAdded++;
|
|
2406
|
+
if (cmp.status === "removed") nodesRemoved++;
|
|
2407
|
+
if (cmp.status === "matched" && cmp.operatorChanged) operatorsChanged++;
|
|
2408
|
+
}
|
|
2409
|
+
return {
|
|
2410
|
+
isCompatible: engineCheck.compatible,
|
|
2411
|
+
incompatibilityReason: engineCheck.reason,
|
|
2412
|
+
hasAnalyzeMismatch,
|
|
2413
|
+
baselineSnapshotId: baseline.id,
|
|
2414
|
+
currentSnapshotId: current.id,
|
|
2415
|
+
summary: {
|
|
2416
|
+
totalCostDelta: calculateMetricDelta(baseline.totalCost, current.totalCost),
|
|
2417
|
+
executionTimeDelta: calculateMetricDelta(
|
|
2418
|
+
baseline.executionTimeMs,
|
|
2419
|
+
current.executionTimeMs
|
|
2420
|
+
),
|
|
2421
|
+
planningTimeDelta: calculateMetricDelta(
|
|
2422
|
+
baseline.planningTimeMs,
|
|
2423
|
+
current.planningTimeMs
|
|
2424
|
+
),
|
|
2425
|
+
nodesAdded,
|
|
2426
|
+
nodesRemoved,
|
|
2427
|
+
operatorsChanged
|
|
2428
|
+
},
|
|
2429
|
+
nodeComparisons
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
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 };
|