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:
+64
@@ -398,6 +398,10 @@ impl App {
|
||||
self.handle_dsl_query_success(&command, &data);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslExplainSucceeded { command, plan } => {
|
||||
self.handle_dsl_explain_success(&command, &plan);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslInsertSucceeded { command, result } => {
|
||||
self.handle_dsl_insert_success(&command, &result);
|
||||
Vec::new()
|
||||
@@ -1201,6 +1205,20 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_dsl_explain_success(
|
||||
&mut self,
|
||||
command: &Command,
|
||||
plan: &crate::db::QueryPlan,
|
||||
) {
|
||||
self.note_ok_summary(command);
|
||||
// ADR-0028 §3: the display SQL, then the plan tree.
|
||||
// `render_explain_plan` returns ready-built `OutputLine`s
|
||||
// so it can carry the per-span styling (ADR-0028 §5).
|
||||
for line in crate::output_render::render_explain_plan(plan, self.mode) {
|
||||
self.push_output(line);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_dsl_insert_success(&mut self, command: &Command, result: &InsertResult) {
|
||||
self.note_ok_summary(command);
|
||||
self.note_system(crate::t!("ok.rows_inserted", count = result.rows_affected));
|
||||
@@ -1416,6 +1434,11 @@ impl App {
|
||||
(Operation::Query, Some(name.as_str()), None)
|
||||
}
|
||||
C::Replay { .. } => (Operation::Replay, None, None),
|
||||
// An `explain` failure (e.g. unknown table) is best
|
||||
// described by the wrapped query it failed to plan.
|
||||
C::Explain { query } => {
|
||||
return self.build_translate_context(query, facts);
|
||||
}
|
||||
// App-lifecycle commands never reach this path —
|
||||
// `dispatch_input` routes them through
|
||||
// `dispatch_app_command` before the DSL execution
|
||||
@@ -2419,6 +2442,47 @@ mod tests {
|
||||
assert!(app.output.iter().any(|l| l.text.starts_with("[ok]")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explain_success_event_renders_display_sql_and_plan_tree() {
|
||||
let mut app = App::new();
|
||||
let cmd = Command::Explain {
|
||||
query: Box::new(Command::ShowData {
|
||||
name: "Customers".to_string(),
|
||||
filter: None,
|
||||
limit: None,
|
||||
}),
|
||||
};
|
||||
let plan = crate::db::QueryPlan {
|
||||
display_sql: "SELECT \"id\" FROM \"Customers\"".to_string(),
|
||||
rows: vec![crate::db::ExplainRow {
|
||||
id: 2,
|
||||
parent: 0,
|
||||
detail: "SCAN Customers".to_string(),
|
||||
}],
|
||||
};
|
||||
app.update(AppEvent::DslExplainSucceeded {
|
||||
command: cmd,
|
||||
plan,
|
||||
});
|
||||
// `[ok] explain Customers` header.
|
||||
assert!(
|
||||
app.output.iter().any(|l| l.text.starts_with("[ok]")
|
||||
&& l.text.contains("explain")),
|
||||
"expected an [ok] explain header",
|
||||
);
|
||||
// The display SQL and the plan node both reach output.
|
||||
assert!(
|
||||
app.output
|
||||
.iter()
|
||||
.any(|l| l.text.contains("SELECT \"id\" FROM \"Customers\"")),
|
||||
"expected the display SQL line",
|
||||
);
|
||||
assert!(
|
||||
app.output.iter().any(|l| l.text.contains("SCAN Customers")),
|
||||
"expected the plan-tree node",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_command_dispatches_replay_action_not_execute_dsl() {
|
||||
// Submitting `replay <path>` must NOT produce an
|
||||
|
||||
Reference in New Issue
Block a user