Skip to content

feat(explain): let plugins register their own EXPLAIN parsers - #688

Merged
debba merged 6 commits into
mainfrom
feat/plugin-explain-parsers
Sep 3, 2026
Merged

feat(explain): let plugins register their own EXPLAIN parsers#688
debba merged 6 commits into
mainfrom
feat/plugin-explain-parsers

Conversation

@debba

@debba debba commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lets a driver plugin ship its own Visual EXPLAIN parser instead of having to pre-parse plans into the host's ExplainPlan shape in Rust. A plugin can now return a raw explain_query payload tagged with an engine and a format, declare a TypeScript parser bundle for that format in .tabularium, and the desktop loads and registers it at runtime.

This is the core half of the SQL Server plugin completion work. The plugin side is TabularisDB/tabularis-sqlserver-plugin#3, and the standalone site side is TabularisDB/explain-plan#2.

What changed

@tabularis/explain (0.1.0 to 0.2.0)

  • New parser registry: registerExplainParser, unregisterExplainParser, getExplainParser, listExplainParsers and the RegisteredExplainParser descriptor (engine, format, label, parse, optional sniff).
  • The built-in Postgres, MySQL and SQLite parsers are now registered through the same registry, so parseRawExplain and source detection dispatch by format instead of a hard-coded switch.
  • Registering an existing format replaces it, which is what a plugin upgrade needs. Unregistering restores detection order deterministically.
  • README documents how a parser package registers itself.

Backend

  • PluginManifest gains an optional explain_parsers array (engine, format, module, optional label), mirrored in plugins/manifest.schema.json and tabularium-extensions.schema.json. Built-in drivers report None.
  • The plugin RPC driver detects a raw explain_query result structurally: engine, format and payload must all be strings. original_query may be omitted or null and is filled from the request. Anything else keeps the historical parsed-plan path, so existing plugins are unaffected.
  • ExplainOutput docs updated to describe both plugin shapes.
  • New plugins/runtime_version.rs: the host now enforces min_runtime_version. load_plugin_from_dir refuses an incompatible plugin with a message naming both versions, which surfaces in the startup error banner, and download_and_install rejects the archive while it is still in the temp dir, covering installs by URL or local file that bypass the marketplace filter. Missing, empty or non-semver floors are logged and treated as compatible. Development builds (debug_assertions) load the plugin anyway and queue a warning that the frontend shows as a bottom-right toast via the new get_plugin_runtime_warnings command, so a plugin declaring the next release as its floor stays testable. Comparison follows semver precedence, so a 0.23.0-nightly.1 host does not satisfy a 0.23.0 floor while 0.23.1-3 does.

Frontend

  • pluginExplainLoader.ts reads each declared module once through the existing read_plugin_file command (which already rejects absolute paths and ..), evaluates the IIFE the same way UI extension bundles are evaluated, resolves the export by exact engine and format, applies the manifest label and registers it. Read, evaluation and descriptor failures are logged per plugin and skipped; parser exceptions during actual parsing still surface through Visual EXPLAIN's normal error handling.
  • PluginSlotProvider exposes the explain API as window.__TABULARIS_EXPLAIN__ for externalized bundles, and unregisters the formats it loaded before every reload so disable and re-enable cycles are deterministic. Plugins are processed in sorted id order.
  • Raw view: src/utils/explainRaw.ts detects XML from the leading tag, switches the Monaco language to xml and indents a single-line document one node per line (quoted attribute values containing > stay intact, leaf text stays inline). SQL Server SHOWPLAN arrives as one line, so without this the raw tab was a wrapped paragraph. JSON, plain text and already formatted XML pass through unchanged.
  • PLUGIN_GUIDE.md section 3c documents the manifest field, the IIFE contract (__tabularis_explain_parser__ global, externalized @tabularis/explain, default export of one descriptor or an array) and the raw explain_query result shape.

Compatibility

  • Plugins that return the parsed plan shape keep working unchanged.
  • Plugins that return the raw shape need a host that includes this change, so they must set min_runtime_version to the first release that ships it (planned as 0.23.0). Older hosts that include this PR refuse such a plugin at install and load time instead of failing in Visual EXPLAIN.
  • The explain_parsers manifest field is additive. The registry validator schema needs the same addition before such a plugin can be submitted.

Verification

  • packages/explain/tests/registry.test.ts: registration, replacement, unregistration, detection order and built-in parity.
  • tests/utils/pluginExplainLoader.test.ts: single read per shared module, array exports, manifest-order registration, export matching, label override, isolation of throwing bundles and invalid exports, reload after unregistration, cancellation during and before a module read.
  • tests/utils/explainRaw.test.ts: language detection, XML indentation including > inside attribute values, declarations, comments and CDATA, pass-through for JSON, text and multi-line XML.
  • src-tauri/src/plugins/tests.rs: min_runtime_version deserialization and the runtime gate (no floor, equal or newer host, older host message, prerelease hosts, non-semver values, development override verdict, warning queue drained once).
  • tests/components/plugins/PluginRuntimeWarningToasts.test.tsx: one toast per queued warning with the plugin id in the title, nothing on an empty queue or outside Tauri, re-drain when the enabled plugin set changes.
  • src-tauri/src/plugins/driver.rs and plugins/tests.rs: raw result detection, original_query validation, fallback to parsed plan for malformed raw objects, manifest deserialization with and without explain_parsers.
  • test and test-postgres CI jobs pass.

Manual validation


return () => {
cancelled = true;
for (const format of new Set(loadedExplainFormats)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Reload teardown temporarily unregisters still-enabled plugins' EXPLAIN parsers

The effect cleanup unregisters every format in loadedExplainFormats (all parsers loaded during the previous pass), so when the enabled set changes the parsers belonging to plugins that are still enabled are removed too, and only re-registered after the async get_plugin_manifestread_plugin_file → eval loop completes. An EXPLAIN issued for a still-enabled raw-explain plugin in that window reaches parseRawExplaingetExplainParser returns null and throws "No EXPLAIN parser registered for format …". The gap is brief and only during a settings change, but it is a user-visible transient failure on the raw path. Consider unregistering only the formats of plugins that are no longer enabled (or re-registering still-enabled parsers before the first await) to keep them available across the reload.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 1 Issue Found | Recommendation: Merge (1 suggestion to consider)

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
src-tauri/src/plugins/installer.rs 377 Installing an incompatible plugin on a dev build queues the runtime warning twice (install gate + hot-load both call enforce_min_runtime_version)
Files Reviewed (incremental: commit 7cb24f3 since 6159ac7)
  • src-tauri/src/plugins/runtime_version.rs
  • src-tauri/src/plugins/installer.rs - 1 suggestion
  • src-tauri/src/plugins/manager.rs
  • src-tauri/src/plugins/tests.rs
  • src-tauri/src/lib.rs
  • src/App.tsx
  • src/components/plugins/PluginRuntimeWarningToasts.tsx
  • tests/components/plugins/PluginRuntimeWarningToasts.test.tsx
  • plugins/PLUGIN_GUIDE.md
  • src/i18n/locales/*.json (en, de, es, fr, it, ja, ko, pt-BR, ru, tl, zh) — devRuntimeWarning key

Fix these issues in Kilo Cloud

Previous Review Summaries (2 snapshots, latest commit 6159ac7)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 6159ac7)

Status: 2 Issues Found | Recommendation: Merge (2 suggestions to consider)

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
src/contexts/PluginSlotProvider.tsx 214 Reload teardown temporarily unregisters still-enabled plugins' EXPLAIN parsers
tests/utils/pluginExplainLoader.test.ts 216 Cancellation test doesn't pin the pre-evaluate guard it's named for
Files Reviewed (11 files)
  • src/contexts/PluginSlotProvider.tsx - 1 suggestion (carried; file unchanged since prior review)
  • src-tauri/src/plugins/runtime_version.rs
  • src-tauri/src/plugins/installer.rs
  • src-tauri/src/plugins/manager.rs
  • src-tauri/src/plugins/mod.rs
  • src-tauri/src/plugins/tests.rs
  • src/utils/explainRaw.ts
  • src/components/explain/VisualExplainView.tsx
  • tests/utils/explainRaw.test.ts
  • tests/utils/pluginExplainLoader.test.ts - 1 suggestion
  • plugins/PLUGIN_GUIDE.md

Fix these issues in Kilo Cloud

Previous review (commit ba0463d)

Status: 1 Issue Found | Recommendation: Merge (1 suggestion to consider)

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
src/contexts/PluginSlotProvider.tsx 214 Reload teardown temporarily unregisters still-enabled plugins' EXPLAIN parsers
Files Reviewed (25 files)
  • packages/explain/README.md
  • packages/explain/package.json
  • packages/explain/src/index.ts
  • packages/explain/src/parsers/builtins.ts
  • packages/explain/src/parsers/source.ts
  • packages/explain/src/raw.ts
  • packages/explain/src/registry.ts
  • packages/explain/tests/registry.test.ts
  • plugins/PLUGIN_GUIDE.md
  • plugins/manifest.schema.json
  • plugins/tabularium-extensions.schema.json
  • src-tauri/src/drivers/driver_trait.rs
  • src-tauri/src/drivers/mysql/mod.rs
  • src-tauri/src/drivers/postgres/mod.rs
  • src-tauri/src/drivers/sqlite/mod.rs
  • src-tauri/src/models.rs
  • src-tauri/src/plugins/commands.rs
  • src-tauri/src/plugins/driver.rs
  • src-tauri/src/plugins/manager.rs
  • src-tauri/src/plugins/tests.rs
  • src-tauri/tests/postgres_integration/parity.rs
  • src/contexts/PluginSlotProvider.tsx - 1 suggestion
  • src/types/plugins.ts
  • src/utils/pluginExplainLoader.ts
  • tests/utils/pluginExplainLoader.test.ts

Fix this issue in Kilo Cloud


Reviewed by glm-5.2 · Input: 66.2K · Output: 34.6K · Cached: 3.9M

A plugin manifest may declare the first Tabularis release it can run on,
but the host never checked it: an older build loaded the plugin and failed
later, for example in Visual EXPLAIN with "No EXPLAIN parser registered".

Add a small semver gate that refuses the plugin up front with a message
naming both versions. It runs in load_plugin_from_dir, where the error
reaches the startup error banner, and in download_and_install before the
bundle leaves the temp dir, covering installs by URL or local file that
bypass the marketplace filter. Missing, empty or non-semver floors are
logged and treated as compatible so a typo cannot brick loading.
The raw view only knew JSON and plain text. SQL Server SHOWPLAN arrives as
a single line of XML, which Monaco showed as one wrapped paragraph. Detect
XML from the leading tag, switch the editor language and indent one node
per line, keeping quoted attribute values containing '>' intact and leaf
text on the same line as its tags. Already multi-line XML, JSON and text
pass through unchanged.

Also add two loader tests for the cancellation path: no registration when
the enabled set changes during a module read, and no read at all when
cancelled before the first module.
);
});

it("stops before evaluating a bundle once loading is cancelled", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: This test doesn't actually pin the pre-evaluateBundle cancellation guard it's named for

The test name claims loading "stops before evaluating a bundle", but none of its three assertions observe whether evaluateBundle ran. The bundle source returned here has no eval-time side effect (it only assigns __tabularis_explain_parser__), and registerExplainParser is gated by a separate checkpoint (pluginExplainLoader.ts:130). So if the post-read / pre-evaluate guard at pluginExplainLoader.ts:103 were deleted, the plugin JS would still execute, yet formats would stay [], invokeMock stay called once, and getExplainParser(\"cancelled-format\") stay null — this test stays green. (The sibling test at line 234 correctly pins the pre-read guard via expect(invokeMock).not.toHaveBeenCalled().)

Consider making the bundle source emit an observable side effect on evaluation (for example, set a sentinel) and asserting it did not fire, so this test actually guards the security-relevant "don't run cancelled plugin bundles" property.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

… a warning toast

A hard refusal makes it impossible to test a plugin that already declares
the next host release as its floor, which is exactly the situation while
that release is being prepared. In debug builds the gate now loads the
plugin anyway, logs the mismatch and queues a runtime warning; release
builds keep refusing.

The frontend drains the queue through get_plugin_runtime_warnings on
startup and whenever the enabled plugin set changes, which is when plugins
are loaded through install or enable, and shows each warning as a toast in
the bottom-right corner with the plugin id in its title.
// The marketplace hides incompatible releases, but archives can also be
// installed by URL or from a local file, so gate the host version here
// while the bundle is still in the temp dir.
if let Err(e) = super::runtime_version::enforce_min_runtime_version(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Installing an incompatible plugin on a dev build queues the runtime warning twice

download_and_install now calls enforce_min_runtime_version, which in a development build returns Ok via a DevOverride and push_runtime_warnings. The install_plugin command then immediately hot-loads the plugin through load_plugin_from_dir (commands.rs:221), which calls enforce_min_runtime_version a second time and pushes the same warning again. Two identical PluginRuntimeWarning entries for one plugin land in the queue, so PluginRuntimeWarningToasts renders two identical dev-override toasts for a single install. Consider having the load path own the warning (e.g., make the install gate in download_and_install only refuse in release builds and skip queueing) or dedupe.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@debba
debba merged commit 399f517 into main Sep 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant