@tabularis/explain 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,13 +53,13 @@ SQLite `EXPLAIN QUERY PLAN` rows:
53
53
  import { parseExplain, parseExplainFor, parseRawExplain } from "@tabularis/explain";
54
54
 
55
55
  // Sniff the format of a pasted blob or an uploaded file:
56
- const plan = parseExplain(raw);
56
+ const plan = await parseExplain(raw);
57
57
 
58
58
  // Or name the engine when the caller knows it:
59
- const hinted = parseExplainFor(raw, "mysql");
59
+ const hinted = await parseExplainFor(raw, "mysql");
60
60
 
61
61
  // Or parse the raw payload a host driver handed over:
62
- const fromDriver = parseRawExplain({ engine, format, payload, original_query });
62
+ const fromDriver = await parseRawExplain({ engine, format, payload, original_query });
63
63
  ```
64
64
 
65
65
  A host obtains the bytes however it likes — a driver, a pasted textarea, an
@@ -67,6 +67,31 @@ 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
+ These helpers are asynchronous because plugin parsers run in isolated guest
90
+ frames. Await `detectFormat`, `detectFormatFor`, `parseExplain`,
91
+ `parseExplainFor`, `parseRawExplain`, and `resolveExplainOutput`.
92
+ Registering the same format again replaces it, which supports plugin upgrades;
93
+ call `unregisterExplainParser(format)` when unloading it.
94
+
70
95
  ## Host requirements for the React entry point
71
96
 
72
97
  The views are presentational but not self-contained. A host must provide:
@@ -79,7 +79,7 @@ interface ExplainMetrics {
79
79
  totalExclusiveTimeMs: number;
80
80
  }
81
81
  /** Metric a bar chart or heat map can be driven by. */
82
- type ExplainMetricKind = "time" | "rows" | "cost" | "buffers";
82
+ type ExplainMetricKind = 'time' | 'rows' | 'cost' | 'buffers';
83
83
  declare const EXPLAIN_METRIC_KINDS: readonly ExplainMetricKind[];
84
84
  /** Compute exclusive/total metrics for every node in `plan`. */
85
85
  declare function computeExplainMetrics(plan: ExplainPlan): ExplainMetrics;
@@ -100,8 +100,8 @@ declare function getAvailableMetricKinds(metrics: ExplainMetrics): ExplainMetric
100
100
  */
101
101
  declare function getDefaultMetricKind(metrics: ExplainMetrics): ExplainMetricKind | null;
102
102
 
103
- type ExplainDiagnosticKind = "hotspot" | "over-estimate" | "under-estimate" | "disk-sort" | "filter-loss" | "large-seq-scan" | "heap-fetches" | "workers-underused" | "high-loops" | "cache-miss" | "never-executed";
104
- type ExplainDiagnosticSeverity = "info" | "warning" | "critical";
103
+ type ExplainDiagnosticKind = 'hotspot' | 'over-estimate' | 'under-estimate' | 'disk-sort' | 'filter-loss' | 'large-seq-scan' | 'heap-fetches' | 'workers-underused' | 'high-loops' | 'cache-miss' | 'never-executed';
104
+ type ExplainDiagnosticSeverity = 'info' | 'warning' | 'critical';
105
105
  interface ExplainDiagnostic {
106
106
  kind: ExplainDiagnosticKind;
107
107
  severity: ExplainDiagnosticSeverity;
@@ -154,4 +154,4 @@ declare function getWorstSeverity(diagnostics: ExplainDiagnostic[]): ExplainDiag
154
154
  /** How many diagnostics of each severity the whole plan produced. */
155
155
  declare function countDiagnosticsBySeverity(diagnostics: Map<string, ExplainDiagnostic[]>): Record<ExplainDiagnosticSeverity, number>;
156
156
 
157
- export { type ExplainPlan as E, type ExplainNode as a, type ExplainMetrics as b, EXPLAIN_DIAGNOSTIC_THRESHOLDS as c, EXPLAIN_METRIC_KINDS as d, type ExplainDiagnostic as e, type ExplainDiagnosticKind as f, type ExplainDiagnosticSeverity as g, type ExplainMetricKind as h, type ExplainNodeMetrics as i, computeExplainMetrics as j, countDiagnosticsBySeverity as k, getAvailableMetricKinds as l, getDefaultMetricKind as m, getMetricMax as n, getMetricValue as o, getNodeDiagnostics as p, getNodeMetrics as q, getPlanDiagnostics as r, getWorstSeverity as s, isMetricAvailable as t };
157
+ export { type ExplainPlan as E, type ExplainNode as a, type ExplainMetrics as b, type ExplainNodeMetrics as c, type ExplainDiagnostic as d, EXPLAIN_DIAGNOSTIC_THRESHOLDS as e, EXPLAIN_METRIC_KINDS as f, type ExplainDiagnosticKind as g, type ExplainDiagnosticSeverity as h, type ExplainMetricKind as i, computeExplainMetrics as j, countDiagnosticsBySeverity as k, getAvailableMetricKinds as l, getDefaultMetricKind as m, getMetricMax as n, getMetricValue as o, getNodeDiagnostics as p, getNodeMetrics as q, getPlanDiagnostics as r, getWorstSeverity as s, isMetricAvailable as t };
package/dist/flow.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Node, Edge } from '@xyflow/react';
2
- import { a as ExplainNode, i as ExplainNodeMetrics, e as ExplainDiagnostic, E as ExplainPlan, b as ExplainMetrics } from './diagnostics-DPSgsjFk.js';
2
+ import { a as ExplainNode, c as ExplainNodeMetrics, d as ExplainDiagnostic, E as ExplainPlan, b as ExplainMetrics } from './diagnostics-DYKMtJZS.js';
3
3
 
4
4
  /**
5
5
  * ReactFlow adapter: turns a plan tree into positioned nodes and edges.
package/dist/flow.js CHANGED
@@ -74,16 +74,10 @@ function computeExplainMetrics(plan) {
74
74
  let maxExclusiveBuffers = 0;
75
75
  let totalExclusiveTimeMs = 0;
76
76
  for (const metrics of order) {
77
- maxExclusiveTimeMs = Math.max(
78
- maxExclusiveTimeMs,
79
- metrics.exclusiveTimeMs ?? 0
80
- );
77
+ maxExclusiveTimeMs = Math.max(maxExclusiveTimeMs, metrics.exclusiveTimeMs ?? 0);
81
78
  maxExclusiveCost = Math.max(maxExclusiveCost, metrics.exclusiveCost ?? 0);
82
79
  maxTotalRows = Math.max(maxTotalRows, metrics.totalRows ?? 0);
83
- maxExclusiveBuffers = Math.max(
84
- maxExclusiveBuffers,
85
- metrics.exclusiveBuffers ?? 0
86
- );
80
+ maxExclusiveBuffers = Math.max(maxExclusiveBuffers, metrics.exclusiveBuffers ?? 0);
87
81
  totalExclusiveTimeMs += metrics.exclusiveTimeMs ?? 0;
88
82
  }
89
83
  if (totalExclusiveTimeMs > 0) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,27 @@
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';
1
+ import { E as ExplainPlan, a as ExplainNode, b as ExplainMetrics, c as ExplainNodeMetrics, d as ExplainDiagnostic } from './diagnostics-DYKMtJZS.js';
2
+ export { e as EXPLAIN_DIAGNOSTIC_THRESHOLDS, f as EXPLAIN_METRIC_KINDS, g as ExplainDiagnosticKind, h as ExplainDiagnosticSeverity, i as ExplainMetricKind, 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-DYKMtJZS.js';
3
+ export { E as EXPLAIN_TABLE_ANALYZE_COLUMNS, a as EXPLAIN_TABLE_COLUMNS, b as ExplainTableColumnId, i as isDefaultExplainTableColumnOrder, m as moveExplainTableColumn, n as normalizeExplainTableColumnOrder, s as shiftExplainTableColumn } from './tableColumns-BWoGh7tK.js';
4
+
5
+ interface RegisteredExplainParser {
6
+ /** Canonical engine id, for example "sqlserver". */
7
+ readonly engine: string;
8
+ /** Globally unique wire-format tag. */
9
+ readonly format: string;
10
+ /** Human label for format pickers. */
11
+ readonly label?: string;
12
+ /** Parse the raw payload or throw an Error. */
13
+ parse(payload: string): ExplainPlan | Promise<ExplainPlan>;
14
+ /** Cheap, side-effect-free source detection. */
15
+ sniff?(payload: string): boolean | Promise<boolean>;
16
+ }
17
+ /** Register a custom parser or replace the effective parser for its format. */
18
+ declare function registerExplainParser(parser: RegisteredExplainParser): void;
19
+ /** Remove a mutable registration, revealing a built-in parser when overridden. */
20
+ declare function unregisterExplainParser(format: string): void;
21
+ /** Return the effective parser for a format. */
22
+ declare function getExplainParser(format: string): RegisteredExplainParser | null;
23
+ /** Return an immutable snapshot of effective parsers in dispatch order. */
24
+ declare function listExplainParsers(): readonly RegisteredExplainParser[];
3
25
 
4
26
  /**
5
27
  * Format detection and dispatch for a raw EXPLAIN payload.
@@ -12,35 +34,32 @@ export { c as EXPLAIN_DIAGNOSTIC_THRESHOLDS, d as EXPLAIN_METRIC_KINDS, e as Exp
12
34
  * inspects them.
13
35
  */
14
36
 
37
+ /** Engines whose source detection is built into this package. */
38
+ type BuiltinExplainEngine = 'postgres' | 'mysql' | 'sqlite';
15
39
  /** 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 =
40
+ type ExplainEngine = BuiltinExplainEngine | (string & {});
41
+ /** Source formats whose detection is built into this package. */
42
+ type BuiltinExplainSourceFormat =
19
43
  /** Postgres `EXPLAIN (FORMAT JSON [, ANALYZE, BUFFERS])` output. */
20
- "postgres-json"
44
+ 'postgres-json'
21
45
  /**
22
46
  * Postgres default `EXPLAIN` output — indentation-based tree with
23
47
  * `cost=X..Y rows=N width=W` headers and optional `actual time` blocks.
24
48
  */
25
- | "postgres-text"
49
+ | 'postgres-text'
26
50
  /**
27
51
  * MySQL / MariaDB `EXPLAIN FORMAT=JSON` or `ANALYZE FORMAT=JSON` output —
28
52
  * a document with a `query_block` key.
29
53
  */
30
- | "mysql-json"
54
+ | 'mysql-json'
31
55
  /** 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 {
56
+ | 'mysql-text';
57
+ /** Supported source format supplied by a built-in or registered parser. */
58
+ type ExplainSourceFormat = BuiltinExplainSourceFormat | (string & {});
59
+ /** A parser for one serialised EXPLAIN payload format. */
60
+ interface ExplainSourceParser extends RegisteredExplainParser {
40
61
  readonly engine: ExplainEngine;
41
62
  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
63
  }
45
64
  /**
46
65
  * Map a driver identifier — the same string carried in `ExplainPlan.driver` —
@@ -54,28 +73,25 @@ declare function explainEngineFromDriverName(name: string): ExplainEngine | null
54
73
  /**
55
74
  * Detect the format of a payload of unknown origin.
56
75
  *
57
- * Recognises the two Postgres shapes only; pass an engine to
58
- * `detectFormatFor` to reach the others.
76
+ * Recognises the two Postgres shapes before trying registered custom sniffers.
59
77
  */
60
- declare function detectFormat(raw: string): ExplainSourceFormat;
78
+ declare function detectFormat(raw: string): Promise<ExplainSourceFormat>;
61
79
  /**
62
80
  * Detect the format of a payload, given what the caller knows about its
63
81
  * origin.
64
82
  *
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`).
83
+ * Built-in hints retain their historical decisions. Custom parsers are tried
84
+ * in registry order and only through their side-effect-free sniffers.
69
85
  */
70
- declare function detectFormatFor(raw: string, engine: ExplainEngine | null): ExplainSourceFormat;
86
+ declare function detectFormatFor(raw: string, engine: ExplainEngine | null): Promise<ExplainSourceFormat>;
71
87
  /**
72
88
  * Parse a payload of unknown origin, sniffing the format.
73
89
  *
74
90
  * Equivalent to `parseExplainFor(raw, null)`.
75
91
  */
76
- declare function parseExplain(raw: string): ExplainPlan;
92
+ declare function parseExplain(raw: string): Promise<ExplainPlan>;
77
93
  /** Parse a payload, using the caller's engine hint when there is one. */
78
- declare function parseExplainFor(raw: string, engine: ExplainEngine | null): ExplainPlan;
94
+ declare function parseExplainFor(raw: string, engine: ExplainEngine | null): Promise<ExplainPlan>;
79
95
  /**
80
96
  * Label a plan that came from a named source (a file, an upload) so the UI
81
97
  * can display "From file: …" without needing a separate field.
@@ -198,12 +214,14 @@ declare function parseSqliteEqpRows(entries: SqliteEqpRow[]): ExplainPlan;
198
214
  * parsers back the desktop app and any standalone visualiser fed a pasted or
199
215
  * uploaded plan.
200
216
  *
201
- * Plugin drivers know engines this package does not, so they keep returning a
202
- * fully-parsed plan; `resolveExplainOutput` accepts both shapes.
217
+ * Plugin drivers can register parsers for formats this package does not know;
218
+ * `resolveExplainOutput` also accepts their historical fully-parsed shape.
203
219
  */
204
220
 
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";
221
+ /** Wire formats supplied by Tabularis built-in drivers. */
222
+ type BuiltinRawExplainFormat = 'postgres-json' | 'mysql-json' | 'mysql-analyze-text' | 'mysql-tabular-rows' | 'sqlite-eqp-rows';
223
+ /** Wire format supplied by a built-in or registered plugin driver. */
224
+ type RawExplainFormat = BuiltinRawExplainFormat | (string & {});
207
225
  /** Raw EXPLAIN output produced by a built-in driver, parsed on this side. */
208
226
  interface RawExplainOutput {
209
227
  /** Driver id of the engine that produced the payload ("postgres", …). */
@@ -218,16 +236,16 @@ interface RawExplainOutput {
218
236
  * built-in driver, or a plan a plugin driver already parsed.
219
237
  */
220
238
  type ExplainQueryOutput = {
221
- kind: "raw";
239
+ kind: 'raw';
222
240
  raw: RawExplainOutput;
223
241
  } | {
224
- kind: "plan";
242
+ kind: 'plan';
225
243
  plan: ExplainPlan;
226
244
  };
227
245
  /** Parse a driver's raw EXPLAIN payload into a plan. */
228
- declare function parseRawExplain(raw: RawExplainOutput): ExplainPlan;
246
+ declare function parseRawExplain(raw: RawExplainOutput): Promise<ExplainPlan>;
229
247
  /** Normalise either shape of `ExplainQueryOutput` into a plan. */
230
- declare function resolveExplainOutput(output: ExplainQueryOutput): ExplainPlan;
248
+ declare function resolveExplainOutput(output: ExplainQueryOutput): Promise<ExplainPlan>;
231
249
 
232
250
  interface ExplainNodeTypeStat {
233
251
  nodeType: string;
@@ -321,5 +339,142 @@ declare function getRowEstimateRatio(node: ExplainNode): number | null;
321
339
  */
322
340
  declare function getExplainPlanSummary(plan: ExplainPlan, metrics?: ExplainMetrics): ExplainPlanSummary;
323
341
  declare function getExplainDriverLegend(plan: ExplainPlan): string[];
342
+ interface ExplainTableRowData {
343
+ index: number;
344
+ depth: number;
345
+ nodeType: string;
346
+ relation: string | null;
347
+ startupCost: number | null;
348
+ totalCost: number | null;
349
+ planRows: number | null;
350
+ actualRows: number | null;
351
+ /** How many times the node ran (`Actual Loops`); null without ANALYZE data. */
352
+ actualLoops: number | null;
353
+ exclusiveTimeMs: number | null;
354
+ /** See {@link getRowEstimateGap}. */
355
+ estimateGap: number | null;
356
+ filter: string | null;
357
+ }
358
+ /**
359
+ * Flatten an ExplainPlan tree into table rows, in tree pre-order, collapsed
360
+ * branches included. See {@link getExplainTableRow} for the row values.
361
+ */
362
+ declare function getExplainTableRows(plan: ExplainPlan, metrics?: ExplainMetrics): ExplainTableRowData[];
363
+
364
+ type ExplainTranslator = (key: string, defaultValue?: string) => string;
365
+ interface ExplainNodeDetailEntry {
366
+ label: string;
367
+ value: string;
368
+ }
369
+ interface ExplainNodeDetailSection {
370
+ title: string;
371
+ titleKey: string;
372
+ entries: ExplainNodeDetailEntry[];
373
+ }
374
+ interface FormatExplainNodeDetailsOptions {
375
+ hasAnalyzeData?: boolean;
376
+ metrics?: ExplainNodeMetrics | null;
377
+ diagnostics?: ExplainDiagnostic[];
378
+ t?: ExplainTranslator;
379
+ includeSectionHeaders?: boolean;
380
+ }
381
+ /**
382
+ * Build structured label/value sections for an EXPLAIN node matching
383
+ * the display in the node details panel. Omit any metric or field
384
+ * that is absent rather than fabricating placeholder values or zeros.
385
+ */
386
+ declare function getExplainNodeDetailSections(node: ExplainNode, options?: {
387
+ hasAnalyzeData?: boolean;
388
+ metrics?: ExplainNodeMetrics | null;
389
+ diagnostics?: ExplainDiagnostic[];
390
+ t?: ExplainTranslator;
391
+ }): ExplainNodeDetailSection[];
392
+ /**
393
+ * Format the displayed details and metrics of an EXPLAIN node into
394
+ * stable, human-readable plain text suitable for clipboard sharing.
395
+ * Omit unavailable metrics rather than inventing zeros.
396
+ */
397
+ declare function formatExplainNodeDetails(node: ExplainNode, options?: FormatExplainNodeDetailsOptions): string;
398
+
399
+ interface ExplainSnapshotContext {
400
+ connectionId?: string;
401
+ connectionName?: string;
402
+ databaseName?: string;
403
+ schema?: string;
404
+ engine: string;
405
+ version?: string;
406
+ }
407
+ interface ExplainSnapshot {
408
+ id: string;
409
+ name: string;
410
+ createdAt: string;
411
+ sql: string;
412
+ context: ExplainSnapshotContext;
413
+ hasAnalyzeData: boolean;
414
+ planningTimeMs: number | null;
415
+ executionTimeMs: number | null;
416
+ totalCost: number | null;
417
+ rawOutput: string | null;
418
+ plan: ExplainPlan;
419
+ isPinned?: boolean;
420
+ }
421
+ interface NodeMetricDelta {
422
+ baselineValue: number | null;
423
+ currentValue: number | null;
424
+ delta: number | null;
425
+ ratio: number | null;
426
+ }
427
+ interface NodeComparison {
428
+ baselineNodeId: string | null;
429
+ currentNodeId: string | null;
430
+ operatorChanged: boolean;
431
+ baselineNodeType: string | null;
432
+ currentNodeType: string | null;
433
+ relation: string | null;
434
+ status: 'matched' | 'added' | 'removed';
435
+ costDelta: NodeMetricDelta;
436
+ timeDelta: NodeMetricDelta;
437
+ rowsDelta: NodeMetricDelta;
438
+ isAmbiguous?: boolean;
439
+ }
440
+ interface PlanComparisonResult {
441
+ isCompatible: boolean;
442
+ incompatibilityReason?: string;
443
+ hasAnalyzeMismatch: boolean;
444
+ baselineSnapshotId: string;
445
+ currentSnapshotId: string;
446
+ summary: {
447
+ totalCostDelta: NodeMetricDelta;
448
+ executionTimeDelta: NodeMetricDelta;
449
+ planningTimeDelta: NodeMetricDelta;
450
+ nodesAdded: number;
451
+ nodesRemoved: number;
452
+ operatorsChanged: number;
453
+ };
454
+ nodeComparisons: NodeComparison[];
455
+ }
456
+ /**
457
+ * Check engine compatibility between baseline and current plans/snapshots.
458
+ * Costs and units are not comparable across different database engines.
459
+ */
460
+ declare function checkEngineCompatibility(baselineEngine: string, currentEngine: string): {
461
+ compatible: boolean;
462
+ reason?: string;
463
+ };
464
+ /**
465
+ * Calculate absolute and relative delta between two nullable metric numbers.
466
+ */
467
+ declare function calculateMetricDelta(baselineValue: number | null | undefined, currentValue: number | null | undefined): NodeMetricDelta;
468
+ /**
469
+ * Align nodes between baseline and current execution plans using deterministic heuristics:
470
+ * 1. Exact tree path match if node types/relations match.
471
+ * 2. Relation and operator matching within relative subtree depth.
472
+ * 3. Identifies added and removed nodes without fabricating metrics.
473
+ */
474
+ declare function alignPlanNodes(baselineRoot: ExplainNode | null, currentRoot: ExplainNode | null, baselineMetrics?: Map<string, ExplainNodeMetrics>, currentMetrics?: Map<string, ExplainNodeMetrics>): NodeComparison[];
475
+ /**
476
+ * Compare two plan snapshots and produce a comprehensive comparison result.
477
+ */
478
+ declare function comparePlanSnapshots(baseline: ExplainSnapshot, current: ExplainSnapshot, baselineMetrics?: Map<string, ExplainNodeMetrics>, currentMetrics?: Map<string, ExplainNodeMetrics>): PlanComparisonResult;
324
479
 
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 };
480
+ export { type BuiltinExplainEngine, type BuiltinExplainSourceFormat, type BuiltinRawExplainFormat, ExplainDiagnostic, type ExplainEngine, type ExplainIndexStat, type ExplainMetricNode, ExplainMetrics, ExplainNode, type ExplainNodeDetailEntry, type ExplainNodeDetailSection, ExplainNodeMetrics, type ExplainNodeTypeStat, ExplainPlan, type ExplainPlanStats, type ExplainPlanSummary, type ExplainQueryOutput, type ExplainRelationStat, type ExplainSnapshot, type ExplainSnapshotContext, type ExplainSourceFormat, type ExplainSourceParser, type ExplainTableRowData, type FormatExplainNodeDetailsOptions, type MysqlTabularRow, type NodeComparison, type NodeCostStyle, NodeIdAllocator, type NodeMetricDelta, type PlanComparisonResult, type RawExplainFormat, type RawExplainOutput, type RegisteredExplainParser, type SqliteEqpRow, alignPlanNodes, buildSqliteTree, calculateMetricDelta, checkEngineCompatibility, comparePlanSnapshots, detectFormat, detectFormatFor, explainEngineFromDriverName, findExplainNode, flattenExplainNodes, formatCost, formatExplainNodeDetails, formatRatio, formatRows, formatTime, getExplainDriverLegend, getExplainNodeDetailSections, getExplainParser, getExplainPlanStats, getExplainPlanSummary, getExplainTableRows, getHeatBarClass, getMaxCost, getMaxTime, getNodeCostStyle, getRowEstimateRatio, hasAnalyzeDataRecursive, listExplainParsers, parseExplain, parseExplainFor, parseMysqlJson, parseMysqlTabularRows, parseMysqlText, parsePostgresJson, parsePostgresText, parseRawExplain, parseSqliteEqpRows, registerExplainParser, resolveExplainOutput, unregisterExplainParser, withSourceLabel };