@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/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(parseMariadbWrapper(item["read_sorted_file"], "Read Sorted File", ids));
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(parseMariadbWrapper(item["materialized"], "Materialized Subquery", ids));
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
- return parserFor(detectFormatFor(raw, engine)).parse(raw);
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 getMismatchMagnitude(node) {
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 = getMismatchMagnitude(node);
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
- export { EXPLAIN_DIAGNOSTIC_THRESHOLDS, EXPLAIN_METRIC_KINDS, NodeIdAllocator, buildSqliteTree, computeExplainMetrics, countDiagnosticsBySeverity, detectFormat, detectFormatFor, explainEngineFromDriverName, findExplainNode, flattenExplainNodes, formatCost, formatRatio, formatRows, formatTime, getAvailableMetricKinds, getDefaultMetricKind, getExplainDriverLegend, getExplainParser, getExplainPlanStats, getExplainPlanSummary, getHeatBarClass, getMaxCost, getMaxTime, getMetricMax, getMetricValue, getNodeCostStyle, getNodeDiagnostics, getNodeMetrics, getPlanDiagnostics, getRowEstimateRatio, getWorstSeverity, hasAnalyzeDataRecursive, isMetricAvailable, listExplainParsers, parseExplain, parseExplainFor, parseMysqlJson, parseMysqlTabularRows, parseMysqlText, parsePostgresJson, parsePostgresText, parseRawExplain, parseSqliteEqpRows, registerExplainParser, resolveExplainOutput, unregisterExplainParser, withSourceLabel };
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 };