@tabularis/explain 0.1.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.
@@ -0,0 +1,325 @@
1
+ import { E as ExplainPlan, a as ExplainNode, b as ExplainMetrics } from './diagnostics-DPSgsjFk.js';
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
+
4
+ /**
5
+ * Format detection and dispatch for a raw EXPLAIN payload.
6
+ *
7
+ * A caller that knows which engine produced the output says so, and the right
8
+ * parser is used directly. A caller that does not — a dropped file, a pasted
9
+ * blob — omits the hint and the format is sniffed.
10
+ *
11
+ * The host stays responsible for obtaining the bytes; this module only
12
+ * inspects them.
13
+ */
14
+
15
+ /** 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 =
19
+ /** Postgres `EXPLAIN (FORMAT JSON [, ANALYZE, BUFFERS])` output. */
20
+ "postgres-json"
21
+ /**
22
+ * Postgres default `EXPLAIN` output — indentation-based tree with
23
+ * `cost=X..Y rows=N width=W` headers and optional `actual time` blocks.
24
+ */
25
+ | "postgres-text"
26
+ /**
27
+ * MySQL / MariaDB `EXPLAIN FORMAT=JSON` or `ANALYZE FORMAT=JSON` output —
28
+ * a document with a `query_block` key.
29
+ */
30
+ | "mysql-json"
31
+ /** MySQL `EXPLAIN ANALYZE` / MariaDB `ANALYZE FORMAT=TEXT` indented tree. */
32
+ | "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 {
40
+ readonly engine: ExplainEngine;
41
+ 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
+ }
45
+ /**
46
+ * Map a driver identifier — the same string carried in `ExplainPlan.driver` —
47
+ * onto an engine.
48
+ *
49
+ * Matching is case-insensitive, and MariaDB maps onto `"mysql"` because they
50
+ * share every plan format. Returns `null` for an unknown name, so an
51
+ * unrecognised driver degrades to sniffing rather than failing.
52
+ */
53
+ declare function explainEngineFromDriverName(name: string): ExplainEngine | null;
54
+ /**
55
+ * Detect the format of a payload of unknown origin.
56
+ *
57
+ * Recognises the two Postgres shapes only; pass an engine to
58
+ * `detectFormatFor` to reach the others.
59
+ */
60
+ declare function detectFormat(raw: string): ExplainSourceFormat;
61
+ /**
62
+ * Detect the format of a payload, given what the caller knows about its
63
+ * origin.
64
+ *
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`).
69
+ */
70
+ declare function detectFormatFor(raw: string, engine: ExplainEngine | null): ExplainSourceFormat;
71
+ /**
72
+ * Parse a payload of unknown origin, sniffing the format.
73
+ *
74
+ * Equivalent to `parseExplainFor(raw, null)`.
75
+ */
76
+ declare function parseExplain(raw: string): ExplainPlan;
77
+ /** Parse a payload, using the caller's engine hint when there is one. */
78
+ declare function parseExplainFor(raw: string, engine: ExplainEngine | null): ExplainPlan;
79
+ /**
80
+ * Label a plan that came from a named source (a file, an upload) so the UI
81
+ * can display "From file: …" without needing a separate field.
82
+ *
83
+ * Takes the display name rather than a path: deriving a basename from a path
84
+ * is the host's job.
85
+ */
86
+ declare function withSourceLabel(plan: ExplainPlan, name: string): ExplainPlan;
87
+
88
+ /**
89
+ * Postgres `EXPLAIN` parsers: `FORMAT JSON` documents and the default
90
+ * indentation-based text output.
91
+ *
92
+ * Pure string -> `ExplainPlan` transformations: no connection, no statement
93
+ * execution, no file system.
94
+ */
95
+
96
+ /**
97
+ * Parse a Postgres `EXPLAIN (FORMAT JSON)` document into an `ExplainPlan`.
98
+ *
99
+ * Postgres emits a top-level JSON array with one element per explained
100
+ * statement. We honour this by picking the first element; each object carries
101
+ * a `Plan` node plus optional `Planning Time` / `Execution Time` timings.
102
+ */
103
+ declare function parsePostgresJson(raw: string): ExplainPlan;
104
+ /** Parse a Postgres text EXPLAIN dump into an `ExplainPlan`. */
105
+ declare function parsePostgresText(raw: string): ExplainPlan;
106
+
107
+ /**
108
+ * Shared building blocks for every plan parser.
109
+ *
110
+ * Pure value -> `ExplainNode` helpers: no host, no driver, no React.
111
+ */
112
+
113
+ /** Hands out sequential `node_N` ids so every node in a plan is unique. */
114
+ declare class NodeIdAllocator {
115
+ private counter;
116
+ next(): string;
117
+ }
118
+ /** Check recursively whether any node in the tree carries actual ANALYZE data. */
119
+ declare function hasAnalyzeDataRecursive(node: ExplainNode): boolean;
120
+
121
+ /**
122
+ * MySQL / MariaDB plan parsers.
123
+ *
124
+ * Covers the two shapes these engines emit that carry a full plan tree:
125
+ * `EXPLAIN FORMAT=JSON` (and MariaDB's `ANALYZE FORMAT=JSON`, with its
126
+ * `filesort` / `temporary_table` / `subquery_cache` wrappers), MariaDB's
127
+ * `ANALYZE FORMAT=TEXT` indented output, plus the tabular `EXPLAIN` rows that
128
+ * the host decodes from the wire and hands over as plain values.
129
+ */
130
+
131
+ /**
132
+ * Parse a MySQL `EXPLAIN FORMAT=JSON` or MariaDB `ANALYZE FORMAT=JSON`
133
+ * document into a plan.
134
+ *
135
+ * Both shapes hang the tree off a `query_block` key, and MariaDB's analysing
136
+ * variant adds `query_optimization.r_total_time_ms` — picked up as the
137
+ * planning time when present, absent for plain `EXPLAIN FORMAT=JSON`.
138
+ *
139
+ * `original_query` is left empty: only the caller knows the statement.
140
+ */
141
+ declare function parseMysqlJson(raw: string): ExplainPlan;
142
+ /**
143
+ * Parse a MySQL `EXPLAIN ANALYZE` / MariaDB `ANALYZE FORMAT=TEXT` indented
144
+ * tree into a plan.
145
+ *
146
+ * `original_query` is left empty: only the caller knows the statement.
147
+ */
148
+ declare function parseMysqlText(raw: string): ExplainPlan;
149
+ /**
150
+ * One decoded row of tabular `EXPLAIN` output, as the host serialises it.
151
+ *
152
+ * The host owns the wire decoding (column lookup, VARBINARY vs VARCHAR); this
153
+ * shape is the boundary between the driver and the parser.
154
+ */
155
+ interface MysqlTabularRow {
156
+ select_type: string;
157
+ table: string | null;
158
+ access_type: string | null;
159
+ possible_keys: string | null;
160
+ key: string | null;
161
+ rows: number | null;
162
+ filtered: number | null;
163
+ extra: string | null;
164
+ }
165
+ /**
166
+ * Build a plan from tabular `EXPLAIN` rows (MySQL/MariaDB without
167
+ * FORMAT=JSON). The flat rows become children of a synthetic `Query` root.
168
+ */
169
+ declare function parseMysqlTabularRows(rows: MysqlTabularRow[]): ExplainPlan;
170
+
171
+ /**
172
+ * SQLite plan parsers.
173
+ *
174
+ * SQLite's `EXPLAIN QUERY PLAN` returns `(id, parent, detail)` triples. The
175
+ * host decodes those rows; everything below turns them into a plan tree and
176
+ * reads the `detail` strings, with no driver in sight.
177
+ */
178
+
179
+ /** One decoded `EXPLAIN QUERY PLAN` row, as the host serialises it. */
180
+ interface SqliteEqpRow {
181
+ id: number;
182
+ parent: number;
183
+ detail: string;
184
+ }
185
+ declare function buildSqliteTree(entries: SqliteEqpRow[], parentId: number, ids: NodeIdAllocator): ExplainNode;
186
+ /**
187
+ * Build a full plan from decoded `EXPLAIN QUERY PLAN` rows, reconstructing
188
+ * the familiar `id|parent|detail` raw text alongside the tree.
189
+ */
190
+ declare function parseSqliteEqpRows(entries: SqliteEqpRow[]): ExplainPlan;
191
+
192
+ /**
193
+ * The boundary between a host driver and the parsers.
194
+ *
195
+ * A built-in driver runs `EXPLAIN`, decides which variant succeeded, and hands
196
+ * over the raw payload untouched — text, JSON, or decoded rows re-serialised
197
+ * as JSON. Everything that *interprets* that payload lives here, so the same
198
+ * parsers back the desktop app and any standalone visualiser fed a pasted or
199
+ * uploaded plan.
200
+ *
201
+ * Plugin drivers know engines this package does not, so they keep returning a
202
+ * fully-parsed plan; `resolveExplainOutput` accepts both shapes.
203
+ */
204
+
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";
207
+ /** Raw EXPLAIN output produced by a built-in driver, parsed on this side. */
208
+ interface RawExplainOutput {
209
+ /** Driver id of the engine that produced the payload ("postgres", …). */
210
+ engine: string;
211
+ format: RawExplainFormat;
212
+ /** The untouched payload: text, a JSON document, or rows as a JSON array. */
213
+ payload: string;
214
+ original_query: string;
215
+ }
216
+ /**
217
+ * What the host's `explain_query_plan` command returns: a raw payload from a
218
+ * built-in driver, or a plan a plugin driver already parsed.
219
+ */
220
+ type ExplainQueryOutput = {
221
+ kind: "raw";
222
+ raw: RawExplainOutput;
223
+ } | {
224
+ kind: "plan";
225
+ plan: ExplainPlan;
226
+ };
227
+ /** Parse a driver's raw EXPLAIN payload into a plan. */
228
+ declare function parseRawExplain(raw: RawExplainOutput): ExplainPlan;
229
+ /** Normalise either shape of `ExplainQueryOutput` into a plan. */
230
+ declare function resolveExplainOutput(output: ExplainQueryOutput): ExplainPlan;
231
+
232
+ interface ExplainNodeTypeStat {
233
+ nodeType: string;
234
+ count: number;
235
+ /** Summed exclusive time, or `null` when no node of this type was timed. */
236
+ exclusiveTimeMs: number | null;
237
+ /** Share of the plan's total exclusive time, 0..1. */
238
+ timeShare: number | null;
239
+ }
240
+ interface ExplainRelationStat {
241
+ relation: string;
242
+ /** How many nodes read this relation. */
243
+ accessCount: number;
244
+ /** Node types that touched it, in first-seen order. */
245
+ nodeTypes: string[];
246
+ exclusiveTimeMs: number | null;
247
+ totalRows: number | null;
248
+ }
249
+ interface ExplainIndexStat {
250
+ indexName: string;
251
+ relation: string | null;
252
+ scanCount: number;
253
+ exclusiveTimeMs: number | null;
254
+ }
255
+ interface ExplainPlanStats {
256
+ nodeCount: number;
257
+ /** Depth of the deepest node, 1 for a single-node plan. */
258
+ maxDepth: number;
259
+ neverExecutedCount: number;
260
+ totalExclusiveTimeMs: number | null;
261
+ nodeTypes: ExplainNodeTypeStat[];
262
+ relations: ExplainRelationStat[];
263
+ indexes: ExplainIndexStat[];
264
+ }
265
+ /**
266
+ * Plan-wide aggregates: which node types dominate the runtime, which relations
267
+ * are read and how often, and which indexes were used.
268
+ */
269
+ declare function getExplainPlanStats(plan: ExplainPlan, metrics: ExplainMetrics): ExplainPlanStats;
270
+
271
+ /**
272
+ * Plan analysis primitives: tree traversal, formatters, heat colours and the
273
+ * headline summary. No rendering library and no notion of the statement that
274
+ * produced the plan.
275
+ */
276
+
277
+ interface NodeCostStyle {
278
+ border: string;
279
+ headerBg: string;
280
+ }
281
+ interface ExplainMetricNode {
282
+ nodeId: string;
283
+ nodeType: string;
284
+ relation: string | null;
285
+ value: number;
286
+ ratio?: number;
287
+ }
288
+ interface ExplainPlanSummary {
289
+ /** Node with the highest cost of its own, children excluded. */
290
+ highestCostNode: ExplainMetricNode | null;
291
+ /** Node that spent the most time in itself, children excluded. */
292
+ slowestNode: ExplainMetricNode | null;
293
+ largestRowMismatchNode: ExplainMetricNode | null;
294
+ sequentialScans: number;
295
+ tempOperations: number;
296
+ }
297
+ /**
298
+ * Heat colour for one node, given the largest value in the plan. Callers pass
299
+ * *exclusive* values: an inclusive one would paint the plan root red in every
300
+ * plan and hide the actual bottleneck.
301
+ */
302
+ declare function getNodeCostStyle(cost: number, maxCost: number): NodeCostStyle;
303
+ /**
304
+ * Fill colour for a proportional bar, on the same yellow-to-red scale as the
305
+ * graph nodes. Callers pass exclusive values, as for {@link getNodeCostStyle}.
306
+ */
307
+ declare function getHeatBarClass(value: number, max: number): string;
308
+ declare function formatCost(n: number): string;
309
+ declare function formatTime(ms: number): string;
310
+ declare function formatRows(n: number): string;
311
+ declare function formatRatio(n: number): string;
312
+ declare function getMaxCost(node: ExplainNode): number;
313
+ declare function getMaxTime(node: ExplainNode): number;
314
+ declare function flattenExplainNodes(root: ExplainNode): ExplainNode[];
315
+ declare function findExplainNode(root: ExplainNode, nodeId: string | null): ExplainNode | null;
316
+ declare function getRowEstimateRatio(node: ExplainNode): number | null;
317
+ /**
318
+ * Headline findings for the overview bar. Cost and time are ranked on each
319
+ * node's own contribution, so the winner is the step to look at rather than
320
+ * whichever node happens to sit closest to the plan root.
321
+ */
322
+ declare function getExplainPlanSummary(plan: ExplainPlan, metrics?: ExplainMetrics): ExplainPlanSummary;
323
+ declare function getExplainDriverLegend(plan: ExplainPlan): string[];
324
+
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 };