@tabularis/explain 0.1.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 +28 -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 +192 -37
- package/dist/index.js +729 -126
- 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"]),
|
|
@@ -925,80 +927,6 @@ function applyTextAttribute(node, content) {
|
|
|
925
927
|
}
|
|
926
928
|
}
|
|
927
929
|
|
|
928
|
-
// src/parsers/source.ts
|
|
929
|
-
var SOURCE_PARSERS = [
|
|
930
|
-
{ engine: "postgres", format: "postgres-json", parse: parsePostgresJson },
|
|
931
|
-
{ engine: "postgres", format: "postgres-text", parse: parsePostgresText },
|
|
932
|
-
{ engine: "mysql", format: "mysql-json", parse: parseMysqlJson },
|
|
933
|
-
{ engine: "mysql", format: "mysql-text", parse: parseMysqlText }
|
|
934
|
-
];
|
|
935
|
-
function parserFor(format) {
|
|
936
|
-
const parser = SOURCE_PARSERS.find((candidate) => candidate.format === format);
|
|
937
|
-
if (parser === void 0) {
|
|
938
|
-
throw new Error(`No parser registered for format '${format}'`);
|
|
939
|
-
}
|
|
940
|
-
return parser;
|
|
941
|
-
}
|
|
942
|
-
function explainEngineFromDriverName(name) {
|
|
943
|
-
switch (name.trim().toLowerCase()) {
|
|
944
|
-
case "postgres":
|
|
945
|
-
case "postgresql":
|
|
946
|
-
case "pg":
|
|
947
|
-
return "postgres";
|
|
948
|
-
case "mysql":
|
|
949
|
-
case "mariadb":
|
|
950
|
-
return "mysql";
|
|
951
|
-
case "sqlite":
|
|
952
|
-
case "sqlite3":
|
|
953
|
-
return "sqlite";
|
|
954
|
-
default:
|
|
955
|
-
return null;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
function detectFormat(raw) {
|
|
959
|
-
return detectFormatFor(raw, null);
|
|
960
|
-
}
|
|
961
|
-
function detectFormatFor(raw, engine) {
|
|
962
|
-
switch (engine) {
|
|
963
|
-
case "postgres":
|
|
964
|
-
case null:
|
|
965
|
-
if (looksLikeJson(raw)) return "postgres-json";
|
|
966
|
-
if (looksLikePostgresText(raw)) return "postgres-text";
|
|
967
|
-
throw new Error(
|
|
968
|
-
"Unsupported EXPLAIN file format: expected Postgres JSON or text output"
|
|
969
|
-
);
|
|
970
|
-
case "mysql":
|
|
971
|
-
if (looksLikeJson(raw)) return "mysql-json";
|
|
972
|
-
if (raw.trim() === "") {
|
|
973
|
-
throw new Error("Unsupported EXPLAIN file format: input is empty");
|
|
974
|
-
}
|
|
975
|
-
return "mysql-text";
|
|
976
|
-
case "sqlite":
|
|
977
|
-
throw new Error(
|
|
978
|
-
"SQLite EXPLAIN QUERY PLAN has no text form here: pass its (id, parent, detail) rows to buildSqliteTree"
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
function looksLikeJson(raw) {
|
|
983
|
-
const trimmed = raw.trimStart();
|
|
984
|
-
return trimmed.startsWith("[") || trimmed.startsWith("{");
|
|
985
|
-
}
|
|
986
|
-
function looksLikePostgresText(raw) {
|
|
987
|
-
return raw.split("\n").some((line) => line.includes("(cost=") && line.includes("width="));
|
|
988
|
-
}
|
|
989
|
-
function parseExplain(raw) {
|
|
990
|
-
return parseExplainFor(raw, null);
|
|
991
|
-
}
|
|
992
|
-
function parseExplainFor(raw, engine) {
|
|
993
|
-
return parserFor(detectFormatFor(raw, engine)).parse(raw);
|
|
994
|
-
}
|
|
995
|
-
function withSourceLabel(plan, name) {
|
|
996
|
-
if (plan.original_query === "") {
|
|
997
|
-
return { ...plan, original_query: `-- loaded from ${name}` };
|
|
998
|
-
}
|
|
999
|
-
return plan;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
930
|
// src/parsers/sqlite.ts
|
|
1003
931
|
function buildSqliteTree(entries, parentId, ids) {
|
|
1004
932
|
const rootEntry = entries.find(
|
|
@@ -1085,44 +1013,234 @@ function parseSqliteDetail(detail) {
|
|
|
1085
1013
|
return [detail, null, null];
|
|
1086
1014
|
}
|
|
1087
1015
|
|
|
1088
|
-
// src/
|
|
1089
|
-
function
|
|
1090
|
-
const plan = parseRawPayload(raw);
|
|
1091
|
-
return {
|
|
1092
|
-
...plan,
|
|
1093
|
-
driver: raw.engine,
|
|
1094
|
-
original_query: raw.original_query
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
function resolveExplainOutput(output) {
|
|
1098
|
-
return output.kind === "plan" ? output.plan : parseRawExplain(output.raw);
|
|
1099
|
-
}
|
|
1100
|
-
function parseRawPayload(raw) {
|
|
1101
|
-
switch (raw.format) {
|
|
1102
|
-
case "postgres-json":
|
|
1103
|
-
return parsePostgresJson(raw.payload);
|
|
1104
|
-
case "mysql-json":
|
|
1105
|
-
return parseMysqlJson(raw.payload);
|
|
1106
|
-
case "mysql-analyze-text":
|
|
1107
|
-
return parseMysqlText(raw.payload);
|
|
1108
|
-
case "mysql-tabular-rows":
|
|
1109
|
-
return parseMysqlTabularRows(parseJsonRows(raw));
|
|
1110
|
-
case "sqlite-eqp-rows":
|
|
1111
|
-
return parseSqliteEqpRows(parseJsonRows(raw));
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
function parseJsonRows(raw) {
|
|
1016
|
+
// src/parsers/builtins.ts
|
|
1017
|
+
function parseJsonRows(payload, format) {
|
|
1115
1018
|
let value;
|
|
1116
1019
|
try {
|
|
1117
|
-
value = JSON.parse(
|
|
1020
|
+
value = JSON.parse(payload);
|
|
1118
1021
|
} catch (err) {
|
|
1119
1022
|
throw new Error(`Failed to parse EXPLAIN rows: ${String(err)}`);
|
|
1120
1023
|
}
|
|
1121
1024
|
if (!Array.isArray(value)) {
|
|
1122
|
-
throw new Error(`EXPLAIN rows payload for '${
|
|
1025
|
+
throw new Error(`EXPLAIN rows payload for '${format}' must be a JSON array`);
|
|
1123
1026
|
}
|
|
1124
1027
|
return value;
|
|
1125
1028
|
}
|
|
1029
|
+
var BUILTIN_EXPLAIN_PARSERS = Object.freeze([
|
|
1030
|
+
Object.freeze({
|
|
1031
|
+
engine: "postgres",
|
|
1032
|
+
format: "postgres-json",
|
|
1033
|
+
parse: parsePostgresJson
|
|
1034
|
+
}),
|
|
1035
|
+
Object.freeze({
|
|
1036
|
+
engine: "postgres",
|
|
1037
|
+
format: "postgres-text",
|
|
1038
|
+
parse: parsePostgresText
|
|
1039
|
+
}),
|
|
1040
|
+
Object.freeze({
|
|
1041
|
+
engine: "mysql",
|
|
1042
|
+
format: "mysql-json",
|
|
1043
|
+
parse: parseMysqlJson
|
|
1044
|
+
}),
|
|
1045
|
+
Object.freeze({
|
|
1046
|
+
engine: "mysql",
|
|
1047
|
+
format: "mysql-text",
|
|
1048
|
+
parse: parseMysqlText
|
|
1049
|
+
}),
|
|
1050
|
+
Object.freeze({
|
|
1051
|
+
engine: "mysql",
|
|
1052
|
+
format: "mysql-analyze-text",
|
|
1053
|
+
parse: parseMysqlText
|
|
1054
|
+
}),
|
|
1055
|
+
Object.freeze({
|
|
1056
|
+
engine: "mysql",
|
|
1057
|
+
format: "mysql-tabular-rows",
|
|
1058
|
+
parse: (payload) => parseMysqlTabularRows(parseJsonRows(payload, "mysql-tabular-rows"))
|
|
1059
|
+
}),
|
|
1060
|
+
Object.freeze({
|
|
1061
|
+
engine: "sqlite",
|
|
1062
|
+
format: "sqlite-eqp-rows",
|
|
1063
|
+
parse: (payload) => parseSqliteEqpRows(parseJsonRows(payload, "sqlite-eqp-rows"))
|
|
1064
|
+
})
|
|
1065
|
+
]);
|
|
1066
|
+
|
|
1067
|
+
// src/registry.ts
|
|
1068
|
+
var builtinParsersByFormat = new Map(
|
|
1069
|
+
BUILTIN_EXPLAIN_PARSERS.map((parser) => [parser.format, parser])
|
|
1070
|
+
);
|
|
1071
|
+
var parserOverlays = /* @__PURE__ */ new Map();
|
|
1072
|
+
var customFormatOrder = [];
|
|
1073
|
+
function registerExplainParser(parser) {
|
|
1074
|
+
validateParser(parser);
|
|
1075
|
+
const replacing = getExplainParser(parser.format) !== null;
|
|
1076
|
+
if (replacing) {
|
|
1077
|
+
console.warn(
|
|
1078
|
+
`EXPLAIN parser format '${parser.format}' is already registered; replacing it.`
|
|
1079
|
+
);
|
|
1080
|
+
} else {
|
|
1081
|
+
customFormatOrder.push(parser.format);
|
|
1082
|
+
}
|
|
1083
|
+
parserOverlays.set(parser.format, parser);
|
|
1084
|
+
}
|
|
1085
|
+
function unregisterExplainParser(format) {
|
|
1086
|
+
if (!parserOverlays.delete(format)) return;
|
|
1087
|
+
if (builtinParsersByFormat.has(format)) return;
|
|
1088
|
+
const index = customFormatOrder.indexOf(format);
|
|
1089
|
+
if (index !== -1) customFormatOrder.splice(index, 1);
|
|
1090
|
+
}
|
|
1091
|
+
function getExplainParser(format) {
|
|
1092
|
+
return parserOverlays.get(format) ?? builtinParsersByFormat.get(format) ?? null;
|
|
1093
|
+
}
|
|
1094
|
+
function listExplainParsers() {
|
|
1095
|
+
const parsers = BUILTIN_EXPLAIN_PARSERS.map(
|
|
1096
|
+
(parser) => parserOverlays.get(parser.format) ?? parser
|
|
1097
|
+
);
|
|
1098
|
+
for (const format of customFormatOrder) {
|
|
1099
|
+
const parser = parserOverlays.get(format);
|
|
1100
|
+
if (parser !== void 0) parsers.push(parser);
|
|
1101
|
+
}
|
|
1102
|
+
return Object.freeze(parsers);
|
|
1103
|
+
}
|
|
1104
|
+
function validateParser(parser) {
|
|
1105
|
+
if (typeof parser !== "object" || parser === null || typeof parser.engine !== "string" || parser.engine.trim() === "" || typeof parser.format !== "string" || parser.format.trim() === "" || typeof parser.parse !== "function") {
|
|
1106
|
+
throw new TypeError(
|
|
1107
|
+
"EXPLAIN parser registration requires non-empty engine and format strings and a parse function"
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/parsers/source.ts
|
|
1113
|
+
var BUILTIN_SOURCE_FORMATS = /* @__PURE__ */ new Set([
|
|
1114
|
+
"postgres-json",
|
|
1115
|
+
"postgres-text",
|
|
1116
|
+
"mysql-json",
|
|
1117
|
+
"mysql-text"
|
|
1118
|
+
]);
|
|
1119
|
+
function parserFor(format) {
|
|
1120
|
+
const parser = getExplainParser(format);
|
|
1121
|
+
if (parser === null) {
|
|
1122
|
+
throw new Error(`No parser registered for format '${format}'`);
|
|
1123
|
+
}
|
|
1124
|
+
return parser;
|
|
1125
|
+
}
|
|
1126
|
+
function explainEngineFromDriverName(name) {
|
|
1127
|
+
const normalizedName = name.trim().toLowerCase();
|
|
1128
|
+
switch (normalizedName) {
|
|
1129
|
+
case "postgres":
|
|
1130
|
+
case "postgresql":
|
|
1131
|
+
case "pg":
|
|
1132
|
+
return "postgres";
|
|
1133
|
+
case "mysql":
|
|
1134
|
+
case "mariadb":
|
|
1135
|
+
return "mysql";
|
|
1136
|
+
case "sqlite":
|
|
1137
|
+
case "sqlite3":
|
|
1138
|
+
return "sqlite";
|
|
1139
|
+
}
|
|
1140
|
+
const parser = listExplainParsers().find(
|
|
1141
|
+
(candidate) => candidate.engine.trim().toLowerCase() === normalizedName
|
|
1142
|
+
);
|
|
1143
|
+
return parser?.engine ?? null;
|
|
1144
|
+
}
|
|
1145
|
+
async function detectFormat(raw) {
|
|
1146
|
+
return await detectFormatFor(raw, null);
|
|
1147
|
+
}
|
|
1148
|
+
async function detectFormatFor(raw, engine) {
|
|
1149
|
+
switch (engine) {
|
|
1150
|
+
case "postgres":
|
|
1151
|
+
return Promise.resolve(detectPostgresFormat(raw));
|
|
1152
|
+
case "mysql":
|
|
1153
|
+
if (looksLikeJson(raw)) return Promise.resolve("mysql-json");
|
|
1154
|
+
if (raw.trim() === "") {
|
|
1155
|
+
throw new Error("Unsupported EXPLAIN file format: input is empty");
|
|
1156
|
+
}
|
|
1157
|
+
return Promise.resolve("mysql-text");
|
|
1158
|
+
case "sqlite":
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
"SQLite EXPLAIN QUERY PLAN has no text form here: pass its (id, parent, detail) rows to buildSqliteTree"
|
|
1161
|
+
);
|
|
1162
|
+
case null: {
|
|
1163
|
+
const builtinFormat = detectPostgresFormatOrNull(raw);
|
|
1164
|
+
if (builtinFormat !== null) return builtinFormat;
|
|
1165
|
+
const customFormat = await sniffRegisteredFormat(raw, null);
|
|
1166
|
+
if (customFormat !== null) return customFormat;
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
"Unsupported EXPLAIN file format: expected Postgres JSON or text output"
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
default: {
|
|
1172
|
+
const format = await sniffRegisteredFormat(raw, engine);
|
|
1173
|
+
if (format !== null) return format;
|
|
1174
|
+
throw new Error(`Unsupported EXPLAIN file format for engine '${engine}'`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
function detectPostgresFormat(raw) {
|
|
1179
|
+
const format = detectPostgresFormatOrNull(raw);
|
|
1180
|
+
if (format !== null) return format;
|
|
1181
|
+
throw new Error("Unsupported EXPLAIN file format: expected Postgres JSON or text output");
|
|
1182
|
+
}
|
|
1183
|
+
function detectPostgresFormatOrNull(raw) {
|
|
1184
|
+
if (looksLikeJson(raw)) return "postgres-json";
|
|
1185
|
+
if (looksLikePostgresText(raw)) return "postgres-text";
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
async function sniffRegisteredFormat(raw, engine) {
|
|
1189
|
+
const normalizedEngine = engine?.trim().toLowerCase() ?? null;
|
|
1190
|
+
for (const parser of listExplainParsers()) {
|
|
1191
|
+
if (normalizedEngine === null && BUILTIN_SOURCE_FORMATS.has(parser.format)) {
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (normalizedEngine !== null && parser.engine.trim().toLowerCase() !== normalizedEngine) {
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (parser.sniff === void 0) continue;
|
|
1198
|
+
try {
|
|
1199
|
+
if (await parser.sniff(raw)) return parser.format;
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
function looksLikeJson(raw) {
|
|
1206
|
+
const trimmed = raw.trimStart();
|
|
1207
|
+
return trimmed.startsWith("[") || trimmed.startsWith("{");
|
|
1208
|
+
}
|
|
1209
|
+
function looksLikePostgresText(raw) {
|
|
1210
|
+
return raw.split("\n").some((line) => line.includes("(cost=") && line.includes("width="));
|
|
1211
|
+
}
|
|
1212
|
+
function parseExplain(raw) {
|
|
1213
|
+
return parseExplainFor(raw, null);
|
|
1214
|
+
}
|
|
1215
|
+
async function parseExplainFor(raw, engine) {
|
|
1216
|
+
const format = await detectFormatFor(raw, engine);
|
|
1217
|
+
return await parserFor(format).parse(raw);
|
|
1218
|
+
}
|
|
1219
|
+
function withSourceLabel(plan, name) {
|
|
1220
|
+
if (plan.original_query === "") {
|
|
1221
|
+
return { ...plan, original_query: `-- loaded from ${name}` };
|
|
1222
|
+
}
|
|
1223
|
+
return plan;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/raw.ts
|
|
1227
|
+
async function parseRawExplain(raw) {
|
|
1228
|
+
const parser = getExplainParser(raw.format);
|
|
1229
|
+
if (parser === null) {
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
`No EXPLAIN parser registered for format '${raw.format}' (engine '${raw.engine}'). Import the parser package for '${raw.engine}' before parsing.`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
const plan = await parser.parse(raw.payload);
|
|
1235
|
+
return {
|
|
1236
|
+
...plan,
|
|
1237
|
+
driver: raw.engine,
|
|
1238
|
+
original_query: raw.original_query
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
function resolveExplainOutput(output) {
|
|
1242
|
+
return output.kind === "plan" ? Promise.resolve(output.plan) : parseRawExplain(output.raw);
|
|
1243
|
+
}
|
|
1126
1244
|
|
|
1127
1245
|
// src/metrics.ts
|
|
1128
1246
|
var EXPLAIN_METRIC_KINDS = [
|
|
@@ -1202,16 +1320,10 @@ function computeExplainMetrics(plan) {
|
|
|
1202
1320
|
let maxExclusiveBuffers = 0;
|
|
1203
1321
|
let totalExclusiveTimeMs = 0;
|
|
1204
1322
|
for (const metrics of order) {
|
|
1205
|
-
maxExclusiveTimeMs = Math.max(
|
|
1206
|
-
maxExclusiveTimeMs,
|
|
1207
|
-
metrics.exclusiveTimeMs ?? 0
|
|
1208
|
-
);
|
|
1323
|
+
maxExclusiveTimeMs = Math.max(maxExclusiveTimeMs, metrics.exclusiveTimeMs ?? 0);
|
|
1209
1324
|
maxExclusiveCost = Math.max(maxExclusiveCost, metrics.exclusiveCost ?? 0);
|
|
1210
1325
|
maxTotalRows = Math.max(maxTotalRows, metrics.totalRows ?? 0);
|
|
1211
|
-
maxExclusiveBuffers = Math.max(
|
|
1212
|
-
maxExclusiveBuffers,
|
|
1213
|
-
metrics.exclusiveBuffers ?? 0
|
|
1214
|
-
);
|
|
1326
|
+
maxExclusiveBuffers = Math.max(maxExclusiveBuffers, metrics.exclusiveBuffers ?? 0);
|
|
1215
1327
|
totalExclusiveTimeMs += metrics.exclusiveTimeMs ?? 0;
|
|
1216
1328
|
}
|
|
1217
1329
|
if (totalExclusiveTimeMs > 0) {
|
|
@@ -1360,7 +1472,7 @@ function getRowEstimateRatio(node) {
|
|
|
1360
1472
|
}
|
|
1361
1473
|
return node.actual_rows / node.plan_rows;
|
|
1362
1474
|
}
|
|
1363
|
-
function
|
|
1475
|
+
function getRowEstimateGap(node) {
|
|
1364
1476
|
const ratio = getRowEstimateRatio(node);
|
|
1365
1477
|
if (ratio == null) {
|
|
1366
1478
|
return null;
|
|
@@ -1406,7 +1518,7 @@ function getExplainPlanSummary(plan, metrics) {
|
|
|
1406
1518
|
};
|
|
1407
1519
|
}
|
|
1408
1520
|
const ratio = getRowEstimateRatio(node);
|
|
1409
|
-
const magnitude =
|
|
1521
|
+
const magnitude = getRowEstimateGap(node);
|
|
1410
1522
|
if (ratio != null && magnitude != null && (largestRowMismatchNode == null || magnitude > largestRowMismatchNode.value)) {
|
|
1411
1523
|
largestRowMismatchNode = {
|
|
1412
1524
|
nodeId: node.id,
|
|
@@ -1450,14 +1562,33 @@ function getExplainDriverLegend(plan) {
|
|
|
1450
1562
|
"editor.visualExplain.mysqlEstimateLegend2"
|
|
1451
1563
|
];
|
|
1452
1564
|
case "sqlite":
|
|
1453
|
-
return [
|
|
1454
|
-
"editor.visualExplain.sqliteLegend1",
|
|
1455
|
-
"editor.visualExplain.sqliteLegend2"
|
|
1456
|
-
];
|
|
1565
|
+
return ["editor.visualExplain.sqliteLegend1", "editor.visualExplain.sqliteLegend2"];
|
|
1457
1566
|
default:
|
|
1458
1567
|
return [];
|
|
1459
1568
|
}
|
|
1460
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
|
+
}
|
|
1461
1592
|
|
|
1462
1593
|
// src/diagnostics.ts
|
|
1463
1594
|
var EXPLAIN_DIAGNOSTIC_THRESHOLDS = {
|
|
@@ -1643,9 +1774,7 @@ function getNodeDiagnostics(node, metrics) {
|
|
|
1643
1774
|
value: formatRows(node.buffers_read)
|
|
1644
1775
|
});
|
|
1645
1776
|
}
|
|
1646
|
-
return diagnostics.sort(
|
|
1647
|
-
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
|
|
1648
|
-
);
|
|
1777
|
+
return diagnostics.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
1649
1778
|
}
|
|
1650
1779
|
function getPlanDiagnostics(plan, metrics) {
|
|
1651
1780
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -1789,4 +1918,478 @@ function compareByTimeThenCount(a, b) {
|
|
|
1789
1918
|
return (b.exclusiveTimeMs ?? 0) - (a.exclusiveTimeMs ?? 0) || b.count - a.count || a.nodeType.localeCompare(b.nodeType);
|
|
1790
1919
|
}
|
|
1791
1920
|
|
|
1792
|
-
|
|
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 };
|