@tabularis/explain 0.1.0 → 0.2.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 CHANGED
@@ -67,6 +67,28 @@ uploaded file — and hands them over untouched. Drivers for engines these
67
67
  parsers do not know (plugin drivers) supply a fully-parsed `ExplainPlan`
68
68
  instead; both shapes flow through `resolveExplainOutput`.
69
69
 
70
+ ## Adding a parser
71
+
72
+ Parser packages register one descriptor for each engine-specific wire format.
73
+ The optional `sniff` function enables source detection; callers with a known
74
+ engine can pass it to `parseExplainFor`.
75
+
76
+ ```ts
77
+ import { registerExplainParser } from "@tabularis/explain";
78
+
79
+ registerExplainParser({
80
+ engine: "acme-db",
81
+ format: "acme-plan-json",
82
+ label: "Acme DB JSON",
83
+ parse: parseAcmePlan,
84
+ sniff: (payload) => payload.trimStart().startsWith('{"acme_plan":'),
85
+ });
86
+ ```
87
+
88
+ Import the parser package before calling `parseRawExplain` or source detection.
89
+ Registering the same format again replaces it, which supports plugin upgrades;
90
+ call `unregisterExplainParser(format)` when unloading it.
91
+
70
92
  ## Host requirements for the React entry point
71
93
 
72
94
  The views are presentational but not self-contained. A host must provide:
package/dist/index.d.ts CHANGED
@@ -1,6 +1,27 @@
1
1
  import { E as ExplainPlan, a as ExplainNode, b as ExplainMetrics } from './diagnostics-DPSgsjFk.js';
2
2
  export { c as EXPLAIN_DIAGNOSTIC_THRESHOLDS, d as EXPLAIN_METRIC_KINDS, e as ExplainDiagnostic, f as ExplainDiagnosticKind, g as ExplainDiagnosticSeverity, h as ExplainMetricKind, i as ExplainNodeMetrics, j as computeExplainMetrics, k as countDiagnosticsBySeverity, l as getAvailableMetricKinds, m as getDefaultMetricKind, n as getMetricMax, o as getMetricValue, p as getNodeDiagnostics, q as getNodeMetrics, r as getPlanDiagnostics, s as getWorstSeverity, t as isMetricAvailable } from './diagnostics-DPSgsjFk.js';
3
3
 
4
+ interface RegisteredExplainParser {
5
+ /** Canonical engine id, for example "sqlserver". */
6
+ readonly engine: string;
7
+ /** Globally unique wire-format tag. */
8
+ readonly format: string;
9
+ /** Human label for format pickers. */
10
+ readonly label?: string;
11
+ /** Parse the raw payload or throw an Error. */
12
+ parse(payload: string): ExplainPlan;
13
+ /** Cheap, side-effect-free source detection. */
14
+ sniff?(payload: string): boolean;
15
+ }
16
+ /** Register a custom parser or replace the effective parser for its format. */
17
+ declare function registerExplainParser(parser: RegisteredExplainParser): void;
18
+ /** Remove a mutable registration, revealing a built-in parser when overridden. */
19
+ declare function unregisterExplainParser(format: string): void;
20
+ /** Return the effective parser for a format. */
21
+ declare function getExplainParser(format: string): RegisteredExplainParser | null;
22
+ /** Return an immutable snapshot of effective parsers in dispatch order. */
23
+ declare function listExplainParsers(): readonly RegisteredExplainParser[];
24
+
4
25
  /**
5
26
  * Format detection and dispatch for a raw EXPLAIN payload.
6
27
  *
@@ -12,10 +33,12 @@ export { c as EXPLAIN_DIAGNOSTIC_THRESHOLDS, d as EXPLAIN_METRIC_KINDS, e as Exp
12
33
  * inspects them.
13
34
  */
14
35
 
36
+ /** Engines whose source detection is built into this package. */
37
+ type BuiltinExplainEngine = "postgres" | "mysql" | "sqlite";
15
38
  /** The engine that produced an EXPLAIN payload, when the caller knows it. */
16
- type ExplainEngine = "postgres" | "mysql" | "sqlite";
17
- /** Supported source formats that a host may hand to `parseExplainFor`. */
18
- type ExplainSourceFormat =
39
+ type ExplainEngine = BuiltinExplainEngine | (string & {});
40
+ /** Source formats whose detection is built into this package. */
41
+ type BuiltinExplainSourceFormat =
19
42
  /** Postgres `EXPLAIN (FORMAT JSON [, ANALYZE, BUFFERS])` output. */
20
43
  "postgres-json"
21
44
  /**
@@ -30,17 +53,12 @@ type ExplainSourceFormat =
30
53
  | "mysql-json"
31
54
  /** MySQL `EXPLAIN ANALYZE` / MariaDB `ANALYZE FORMAT=TEXT` indented tree. */
32
55
  | "mysql-text";
33
- /**
34
- * A parser for one serialised EXPLAIN payload format.
35
- *
36
- * Every engine exposes its formats through this same interface so dispatch,
37
- * sniffing and hosts handle them uniformly.
38
- */
39
- interface ExplainSourceParser {
56
+ /** Supported source format supplied by a built-in or registered parser. */
57
+ type ExplainSourceFormat = BuiltinExplainSourceFormat | (string & {});
58
+ /** A parser for one serialised EXPLAIN payload format. */
59
+ interface ExplainSourceParser extends RegisteredExplainParser {
40
60
  readonly engine: ExplainEngine;
41
61
  readonly format: ExplainSourceFormat;
42
- /** Turn a raw payload into a plan; throws `Error` when the payload does not fit. */
43
- parse(raw: string): ExplainPlan;
44
62
  }
45
63
  /**
46
64
  * Map a driver identifier — the same string carried in `ExplainPlan.driver` —
@@ -54,18 +72,15 @@ declare function explainEngineFromDriverName(name: string): ExplainEngine | null
54
72
  /**
55
73
  * Detect the format of a payload of unknown origin.
56
74
  *
57
- * Recognises the two Postgres shapes only; pass an engine to
58
- * `detectFormatFor` to reach the others.
75
+ * Recognises the two Postgres shapes before trying registered custom sniffers.
59
76
  */
60
77
  declare function detectFormat(raw: string): ExplainSourceFormat;
61
78
  /**
62
79
  * Detect the format of a payload, given what the caller knows about its
63
80
  * origin.
64
81
  *
65
- * With an engine the choice is between that engine's own formats. Without
66
- * one, behaviour is unchanged from `detectFormat`: JSON is recognised by the
67
- * leading `[` or `{`, and the text form by a Postgres cost header
68
- * (`cost=X..Y rows=N width=W`).
82
+ * Built-in hints retain their historical decisions. Custom parsers are tried
83
+ * in registry order and only through their side-effect-free sniffers.
69
84
  */
70
85
  declare function detectFormatFor(raw: string, engine: ExplainEngine | null): ExplainSourceFormat;
71
86
  /**
@@ -198,12 +213,14 @@ declare function parseSqliteEqpRows(entries: SqliteEqpRow[]): ExplainPlan;
198
213
  * parsers back the desktop app and any standalone visualiser fed a pasted or
199
214
  * uploaded plan.
200
215
  *
201
- * Plugin drivers know engines this package does not, so they keep returning a
202
- * fully-parsed plan; `resolveExplainOutput` accepts both shapes.
216
+ * Plugin drivers can register parsers for formats this package does not know;
217
+ * `resolveExplainOutput` also accepts their historical fully-parsed shape.
203
218
  */
204
219
 
205
- /** Wire formats a built-in driver can hand over. */
206
- type RawExplainFormat = "postgres-json" | "mysql-json" | "mysql-analyze-text" | "mysql-tabular-rows" | "sqlite-eqp-rows";
220
+ /** Wire formats supplied by Tabularis built-in drivers. */
221
+ type BuiltinRawExplainFormat = "postgres-json" | "mysql-json" | "mysql-analyze-text" | "mysql-tabular-rows" | "sqlite-eqp-rows";
222
+ /** Wire format supplied by a built-in or registered plugin driver. */
223
+ type RawExplainFormat = BuiltinRawExplainFormat | (string & {});
207
224
  /** Raw EXPLAIN output produced by a built-in driver, parsed on this side. */
208
225
  interface RawExplainOutput {
209
226
  /** Driver id of the engine that produced the payload ("postgres", …). */
@@ -322,4 +339,4 @@ declare function getRowEstimateRatio(node: ExplainNode): number | null;
322
339
  declare function getExplainPlanSummary(plan: ExplainPlan, metrics?: ExplainMetrics): ExplainPlanSummary;
323
340
  declare function getExplainDriverLegend(plan: ExplainPlan): string[];
324
341
 
325
- export { type ExplainEngine, type ExplainIndexStat, type ExplainMetricNode, ExplainMetrics, ExplainNode, type ExplainNodeTypeStat, ExplainPlan, type ExplainPlanStats, type ExplainPlanSummary, type ExplainQueryOutput, type ExplainRelationStat, type ExplainSourceFormat, type ExplainSourceParser, type MysqlTabularRow, type NodeCostStyle, NodeIdAllocator, type RawExplainFormat, type RawExplainOutput, type SqliteEqpRow, buildSqliteTree, detectFormat, detectFormatFor, explainEngineFromDriverName, findExplainNode, flattenExplainNodes, formatCost, formatRatio, formatRows, formatTime, getExplainDriverLegend, getExplainPlanStats, getExplainPlanSummary, getHeatBarClass, getMaxCost, getMaxTime, getNodeCostStyle, getRowEstimateRatio, hasAnalyzeDataRecursive, parseExplain, parseExplainFor, parseMysqlJson, parseMysqlTabularRows, parseMysqlText, parsePostgresJson, parsePostgresText, parseRawExplain, parseSqliteEqpRows, resolveExplainOutput, withSourceLabel };
342
+ export { type BuiltinExplainEngine, type BuiltinExplainSourceFormat, type BuiltinRawExplainFormat, type ExplainEngine, type ExplainIndexStat, type ExplainMetricNode, ExplainMetrics, ExplainNode, type ExplainNodeTypeStat, ExplainPlan, type ExplainPlanStats, type ExplainPlanSummary, type ExplainQueryOutput, type ExplainRelationStat, type ExplainSourceFormat, type ExplainSourceParser, type MysqlTabularRow, type NodeCostStyle, NodeIdAllocator, type RawExplainFormat, type RawExplainOutput, type RegisteredExplainParser, type SqliteEqpRow, buildSqliteTree, detectFormat, detectFormatFor, explainEngineFromDriverName, findExplainNode, flattenExplainNodes, formatCost, formatRatio, formatRows, formatTime, getExplainDriverLegend, getExplainParser, getExplainPlanStats, getExplainPlanSummary, getHeatBarClass, getMaxCost, getMaxTime, getNodeCostStyle, getRowEstimateRatio, hasAnalyzeDataRecursive, listExplainParsers, parseExplain, parseExplainFor, parseMysqlJson, parseMysqlTabularRows, parseMysqlText, parsePostgresJson, parsePostgresText, parseRawExplain, parseSqliteEqpRows, registerExplainParser, resolveExplainOutput, unregisterExplainParser, withSourceLabel };
package/dist/index.js CHANGED
@@ -925,80 +925,6 @@ function applyTextAttribute(node, content) {
925
925
  }
926
926
  }
927
927
 
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
928
  // src/parsers/sqlite.ts
1003
929
  function buildSqliteTree(entries, parentId, ids) {
1004
930
  const rootEntry = entries.find(
@@ -1085,9 +1011,230 @@ function parseSqliteDetail(detail) {
1085
1011
  return [detail, null, null];
1086
1012
  }
1087
1013
 
1014
+ // src/parsers/builtins.ts
1015
+ function parseJsonRows(payload, format) {
1016
+ let value;
1017
+ try {
1018
+ value = JSON.parse(payload);
1019
+ } catch (err) {
1020
+ throw new Error(`Failed to parse EXPLAIN rows: ${String(err)}`);
1021
+ }
1022
+ if (!Array.isArray(value)) {
1023
+ throw new Error(`EXPLAIN rows payload for '${format}' must be a JSON array`);
1024
+ }
1025
+ return value;
1026
+ }
1027
+ var BUILTIN_EXPLAIN_PARSERS = Object.freeze([
1028
+ Object.freeze({
1029
+ engine: "postgres",
1030
+ format: "postgres-json",
1031
+ parse: parsePostgresJson
1032
+ }),
1033
+ Object.freeze({
1034
+ engine: "postgres",
1035
+ format: "postgres-text",
1036
+ parse: parsePostgresText
1037
+ }),
1038
+ Object.freeze({
1039
+ engine: "mysql",
1040
+ format: "mysql-json",
1041
+ parse: parseMysqlJson
1042
+ }),
1043
+ Object.freeze({
1044
+ engine: "mysql",
1045
+ format: "mysql-text",
1046
+ parse: parseMysqlText
1047
+ }),
1048
+ Object.freeze({
1049
+ engine: "mysql",
1050
+ format: "mysql-analyze-text",
1051
+ parse: parseMysqlText
1052
+ }),
1053
+ Object.freeze({
1054
+ engine: "mysql",
1055
+ format: "mysql-tabular-rows",
1056
+ parse: (payload) => parseMysqlTabularRows(
1057
+ parseJsonRows(payload, "mysql-tabular-rows")
1058
+ )
1059
+ }),
1060
+ Object.freeze({
1061
+ engine: "sqlite",
1062
+ format: "sqlite-eqp-rows",
1063
+ parse: (payload) => parseSqliteEqpRows(
1064
+ parseJsonRows(payload, "sqlite-eqp-rows")
1065
+ )
1066
+ })
1067
+ ]);
1068
+
1069
+ // src/registry.ts
1070
+ var builtinParsersByFormat = new Map(
1071
+ BUILTIN_EXPLAIN_PARSERS.map((parser) => [parser.format, parser])
1072
+ );
1073
+ var parserOverlays = /* @__PURE__ */ new Map();
1074
+ var customFormatOrder = [];
1075
+ function registerExplainParser(parser) {
1076
+ validateParser(parser);
1077
+ const replacing = getExplainParser(parser.format) !== null;
1078
+ if (replacing) {
1079
+ console.warn(
1080
+ `EXPLAIN parser format '${parser.format}' is already registered; replacing it.`
1081
+ );
1082
+ } else {
1083
+ customFormatOrder.push(parser.format);
1084
+ }
1085
+ parserOverlays.set(parser.format, parser);
1086
+ }
1087
+ function unregisterExplainParser(format) {
1088
+ if (!parserOverlays.delete(format)) return;
1089
+ if (builtinParsersByFormat.has(format)) return;
1090
+ const index = customFormatOrder.indexOf(format);
1091
+ if (index !== -1) customFormatOrder.splice(index, 1);
1092
+ }
1093
+ function getExplainParser(format) {
1094
+ return parserOverlays.get(format) ?? builtinParsersByFormat.get(format) ?? null;
1095
+ }
1096
+ function listExplainParsers() {
1097
+ const parsers = BUILTIN_EXPLAIN_PARSERS.map(
1098
+ (parser) => parserOverlays.get(parser.format) ?? parser
1099
+ );
1100
+ for (const format of customFormatOrder) {
1101
+ const parser = parserOverlays.get(format);
1102
+ if (parser !== void 0) parsers.push(parser);
1103
+ }
1104
+ return Object.freeze(parsers);
1105
+ }
1106
+ function validateParser(parser) {
1107
+ if (typeof parser !== "object" || parser === null || typeof parser.engine !== "string" || parser.engine.trim() === "" || typeof parser.format !== "string" || parser.format.trim() === "" || typeof parser.parse !== "function") {
1108
+ throw new TypeError(
1109
+ "EXPLAIN parser registration requires non-empty engine and format strings and a parse function"
1110
+ );
1111
+ }
1112
+ }
1113
+
1114
+ // src/parsers/source.ts
1115
+ var BUILTIN_SOURCE_FORMATS = /* @__PURE__ */ new Set([
1116
+ "postgres-json",
1117
+ "postgres-text",
1118
+ "mysql-json",
1119
+ "mysql-text"
1120
+ ]);
1121
+ function parserFor(format) {
1122
+ const parser = getExplainParser(format);
1123
+ if (parser === null) {
1124
+ throw new Error(`No parser registered for format '${format}'`);
1125
+ }
1126
+ return parser;
1127
+ }
1128
+ function explainEngineFromDriverName(name) {
1129
+ const normalizedName = name.trim().toLowerCase();
1130
+ switch (normalizedName) {
1131
+ case "postgres":
1132
+ case "postgresql":
1133
+ case "pg":
1134
+ return "postgres";
1135
+ case "mysql":
1136
+ case "mariadb":
1137
+ return "mysql";
1138
+ case "sqlite":
1139
+ case "sqlite3":
1140
+ return "sqlite";
1141
+ }
1142
+ const parser = listExplainParsers().find(
1143
+ (candidate) => candidate.engine.trim().toLowerCase() === normalizedName
1144
+ );
1145
+ return parser?.engine ?? null;
1146
+ }
1147
+ function detectFormat(raw) {
1148
+ return detectFormatFor(raw, null);
1149
+ }
1150
+ function detectFormatFor(raw, engine) {
1151
+ switch (engine) {
1152
+ case "postgres":
1153
+ return detectPostgresFormat(raw);
1154
+ case "mysql":
1155
+ if (looksLikeJson(raw)) return "mysql-json";
1156
+ if (raw.trim() === "") {
1157
+ throw new Error("Unsupported EXPLAIN file format: input is empty");
1158
+ }
1159
+ return "mysql-text";
1160
+ case "sqlite":
1161
+ throw new Error(
1162
+ "SQLite EXPLAIN QUERY PLAN has no text form here: pass its (id, parent, detail) rows to buildSqliteTree"
1163
+ );
1164
+ case null: {
1165
+ const builtinFormat = detectPostgresFormatOrNull(raw);
1166
+ if (builtinFormat !== null) return builtinFormat;
1167
+ const customFormat = sniffRegisteredFormat(raw, null);
1168
+ if (customFormat !== null) return customFormat;
1169
+ throw new Error(
1170
+ "Unsupported EXPLAIN file format: expected Postgres JSON or text output"
1171
+ );
1172
+ }
1173
+ default: {
1174
+ const format = sniffRegisteredFormat(raw, engine);
1175
+ if (format !== null) return format;
1176
+ throw new Error(`Unsupported EXPLAIN file format for engine '${engine}'`);
1177
+ }
1178
+ }
1179
+ }
1180
+ function detectPostgresFormat(raw) {
1181
+ const format = detectPostgresFormatOrNull(raw);
1182
+ if (format !== null) return format;
1183
+ throw new Error(
1184
+ "Unsupported EXPLAIN file format: expected Postgres JSON or text output"
1185
+ );
1186
+ }
1187
+ function detectPostgresFormatOrNull(raw) {
1188
+ if (looksLikeJson(raw)) return "postgres-json";
1189
+ if (looksLikePostgresText(raw)) return "postgres-text";
1190
+ return null;
1191
+ }
1192
+ function sniffRegisteredFormat(raw, engine) {
1193
+ const normalizedEngine = engine?.trim().toLowerCase() ?? null;
1194
+ for (const parser of listExplainParsers()) {
1195
+ if (normalizedEngine === null && BUILTIN_SOURCE_FORMATS.has(parser.format)) {
1196
+ continue;
1197
+ }
1198
+ if (normalizedEngine !== null && parser.engine.trim().toLowerCase() !== normalizedEngine) {
1199
+ continue;
1200
+ }
1201
+ if (parser.sniff === void 0) continue;
1202
+ try {
1203
+ if (parser.sniff(raw)) return parser.format;
1204
+ } catch {
1205
+ }
1206
+ }
1207
+ return null;
1208
+ }
1209
+ function looksLikeJson(raw) {
1210
+ const trimmed = raw.trimStart();
1211
+ return trimmed.startsWith("[") || trimmed.startsWith("{");
1212
+ }
1213
+ function looksLikePostgresText(raw) {
1214
+ return raw.split("\n").some((line) => line.includes("(cost=") && line.includes("width="));
1215
+ }
1216
+ function parseExplain(raw) {
1217
+ return parseExplainFor(raw, null);
1218
+ }
1219
+ function parseExplainFor(raw, engine) {
1220
+ return parserFor(detectFormatFor(raw, engine)).parse(raw);
1221
+ }
1222
+ function withSourceLabel(plan, name) {
1223
+ if (plan.original_query === "") {
1224
+ return { ...plan, original_query: `-- loaded from ${name}` };
1225
+ }
1226
+ return plan;
1227
+ }
1228
+
1088
1229
  // src/raw.ts
1089
1230
  function parseRawExplain(raw) {
1090
- const plan = parseRawPayload(raw);
1231
+ const parser = getExplainParser(raw.format);
1232
+ if (parser === null) {
1233
+ throw new Error(
1234
+ `No EXPLAIN parser registered for format '${raw.format}' (engine '${raw.engine}'). Import the parser package for '${raw.engine}' before parsing.`
1235
+ );
1236
+ }
1237
+ const plan = parser.parse(raw.payload);
1091
1238
  return {
1092
1239
  ...plan,
1093
1240
  driver: raw.engine,
@@ -1097,32 +1244,6 @@ function parseRawExplain(raw) {
1097
1244
  function resolveExplainOutput(output) {
1098
1245
  return output.kind === "plan" ? output.plan : parseRawExplain(output.raw);
1099
1246
  }
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) {
1115
- let value;
1116
- try {
1117
- value = JSON.parse(raw.payload);
1118
- } catch (err) {
1119
- throw new Error(`Failed to parse EXPLAIN rows: ${String(err)}`);
1120
- }
1121
- if (!Array.isArray(value)) {
1122
- throw new Error(`EXPLAIN rows payload for '${raw.format}' must be a JSON array`);
1123
- }
1124
- return value;
1125
- }
1126
1247
 
1127
1248
  // src/metrics.ts
1128
1249
  var EXPLAIN_METRIC_KINDS = [
@@ -1789,4 +1910,4 @@ function compareByTimeThenCount(a, b) {
1789
1910
  return (b.exclusiveTimeMs ?? 0) - (a.exclusiveTimeMs ?? 0) || b.count - a.count || a.nodeType.localeCompare(b.nodeType);
1790
1911
  }
1791
1912
 
1792
- export { EXPLAIN_DIAGNOSTIC_THRESHOLDS, EXPLAIN_METRIC_KINDS, NodeIdAllocator, buildSqliteTree, computeExplainMetrics, countDiagnosticsBySeverity, detectFormat, detectFormatFor, explainEngineFromDriverName, findExplainNode, flattenExplainNodes, formatCost, formatRatio, formatRows, formatTime, getAvailableMetricKinds, getDefaultMetricKind, getExplainDriverLegend, getExplainPlanStats, getExplainPlanSummary, getHeatBarClass, getMaxCost, getMaxTime, getMetricMax, getMetricValue, getNodeCostStyle, getNodeDiagnostics, getNodeMetrics, getPlanDiagnostics, getRowEstimateRatio, getWorstSeverity, hasAnalyzeDataRecursive, isMetricAvailable, parseExplain, parseExplainFor, parseMysqlJson, parseMysqlTabularRows, parseMysqlText, parsePostgresJson, parsePostgresText, parseRawExplain, parseSqliteEqpRows, resolveExplainOutput, withSourceLabel };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tabularis/explain",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Parse, analyse and visualise a database EXPLAIN plan. Takes raw EXPLAIN output; never runs a query.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/TabularisDB/tabularis/tree/main/packages/explain",