explain: explain command end to end (ADR-0028 steps 2–3)

Add the `explain` prefix command — `explain show data`,
`explain update`, `explain delete` — from grammar through to a
rendered plan tree.

- Grammar: an `EXPLAIN` CommandNode whose shape is a Choice over
  the three explainable query shapes, referenced (not
  duplicated) through `Subgrammar`. `Command::Explain { query:
  Box<Self> }`; `build_show_data` is extracted so the role-based
  builders serve both standalone and explain-wrapped commands.
- Worker: SQL construction is split out of do_query_data /
  do_update / do_delete into `build_*_sql`, so EXPLAIN QUERY
  PLAN runs the exact same statement. `Request::ExplainPlan` /
  `do_explain_plan` capture the plan; `QueryPlan` / `ExplainRow`
  carry it back. EXPLAIN QUERY PLAN never executes, so
  explaining update/delete changes nothing.
- Display SQL: the executed statement with `?N` parameters
  inlined as standard-SQL literals via a quote-aware scan.
- Render: `render_explain_plan` draws the box-drawing plan tree
  (plain output; ADR-0028 step 4 adds the styled tree).
- Catalog: `parse.usage.explain` and the `help.data.explain`
  entry, so `explain` shows up in the in-app `help` listing.

1151 tests pass (+18); clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-19 12:38:02 +00:00
parent c1fcf28e04
commit d17addddd7
11 changed files with 836 additions and 67 deletions
+14
View File
@@ -166,6 +166,16 @@ pub enum Command {
Replay {
path: String,
},
/// Capture and display the query plan for an explainable
/// command without executing it (ADR-0028). The inner
/// `Command` is an ordinary parsed `ShowData` / `Update` /
/// `Delete`; the runtime recognizes the `Explain` wrapper
/// and routes it to the plan path instead of normal
/// execution. Because `EXPLAIN QUERY PLAN` never runs the
/// statement, explaining a destructive command is safe.
Explain {
query: Box<Self>,
},
/// App-lifecycle command (per ADR-0003). These work in both
/// simple and advanced modes; the dispatcher branches on the
/// `Command::App(...)` variant before mode-specific routing.
@@ -457,6 +467,7 @@ impl Command {
Self::Delete { .. } => "delete from",
Self::ShowData { .. } => "show data",
Self::Replay { .. } => "replay",
Self::Explain { .. } => "explain",
Self::App(app) => match app {
AppCommand::Quit => "quit",
AppCommand::Help => "help",
@@ -514,6 +525,9 @@ impl Command {
// Replay isn't tied to a single table; the path is
// the most identifying thing for log output.
Self::Replay { path } => path,
// Explain forwards to the wrapped query — the table
// the plan is about is the inner command's table.
Self::Explain { query } => query.target_table(),
// App commands aren't tied to schema entities — the
// verb is the most identifying thing. The
// display_subject override below provides a richer