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 -1
View File
@@ -30,7 +30,7 @@ use crate::app::App;
use crate::cli::Args;
use crate::db::{
AddColumnResult, ChangeColumnTypeResult, DataResult, Database, DbError, DeleteResult,
DropColumnResult, InsertResult, TableDescription, UpdateResult,
DropColumnResult, InsertResult, QueryPlan, TableDescription, UpdateResult,
};
use crate::dsl::Command;
use crate::dsl::walker::Severity;
@@ -1143,6 +1143,10 @@ fn spawn_dsl_dispatch(
command: command.clone(),
data,
},
Ok(CommandOutcome::QueryPlan(plan)) => AppEvent::DslExplainSucceeded {
command: command.clone(),
plan,
},
Ok(CommandOutcome::Insert(result)) => AppEvent::DslInsertSucceeded {
command: command.clone(),
result,
@@ -1490,6 +1494,7 @@ fn parse_qualified_target(message: &str) -> Option<(String, String)> {
enum CommandOutcome {
Schema(Option<TableDescription>),
Query(DataResult),
QueryPlan(QueryPlan),
Insert(InsertResult),
Update(UpdateResult),
Delete(DeleteResult),
@@ -1797,6 +1802,14 @@ async fn execute_command_typed(
.query_data(name, filter, limit, src)
.await
.map(CommandOutcome::Query),
// `EXPLAIN QUERY PLAN` never executes the wrapped
// statement (ADR-0028 §2), so explaining a destructive
// command is safe. `src` is unused here — explain is a
// diagnostic and is not written to `history.log`.
Command::Explain { query } => database
.explain_query_plan(*query)
.await
.map(CommandOutcome::QueryPlan),
// `replay` is parsed as a DSL command but routed by
// App::dispatch_dsl as `Action::Replay` rather than
// `Action::ExecuteDsl`; it never reaches the worker