@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 +22 -0
- package/dist/index.d.ts +40 -23
- package/dist/index.js +223 -102
- package/package.json +1 -1
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 =
|
|
17
|
-
/**
|
|
18
|
-
type
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
*
|
|
66
|
-
*
|
|
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
|
|
202
|
-
*
|
|
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
|
|
206
|
-
type
|
|
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
|
|
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.
|
|
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",
|