Interim handoff. ADR-0028 (query plans / explain) is started:
step 1 (the styled-output-line mechanism, 03d8a09) is done and
committed. handoff-20 carries the full validated build plan
for steps 2-5 with file:line anchors and three implementation
gotchas (the const/static Subgrammar wrinkle, build_show's
positional dispatch, and why steps 2+3 must land as one
commit) so a fresh session implements them without
re-exploring.
11 KiB
Session handoff — 2026-05-19 (20)
Twentieth handover. Interim handoff: this session
finished ADR-0027 (§1), fixed two manual-testing bugs (§1),
and started ADR-0028 — query plans / explain. ADR-0028
step 1 of 5 is done and committed; steps 2–5 are planned
in full detail (§3) and not yet built. A fresh session can
implement them from this file + the ADR without re-exploring.
State at handoff
Branch: main. Working tree clean. 12 commits since
handoff-18 (39b92a7), all local — push asynchronously, not
blocking.
<this file> docs: handoff 20 — ADR-0028 step 1 done, 2-5 planned
03d8a09 ui: styled-output-line mechanism (ADR-0028 step 1)
a1e4932 docs: handoff 19 update — both manual-testing bugs fixed
3a40ae2 runtime: don't record an unmodified temp as the --resume target
f239ca5 walker: keep optional trailing flags completable after `--`
0e5f226 docs: handoff 19 — ADR-0027 highlight/hint wiring finished
c1c9f6c runtime: extract the indicator debounce into a tested state machine
400fb71 ui: surface diagnostics in the ambient hint panel (ADR-0027 §2)
bbfb70c ui: overlay diagnostic spans on the input field (ADR-0027 §2)
437b2f2 walker: flag LIKE on a numeric column (ADR-0027 Amendment 1)
3912fb5 walker: precise per-literal spans for expression WARNINGs
426e801 command: Operand carries a source span
Tests: 1133 passing, 0 failing, 1 ignored (cargo test). The ignored test is the long-standing ```ignore
doc-test in src/friendly/mod.rs. Typing-surface matrix:
161 cells, unchanged.
Clippy: clean (cargo clippy --all-targets -- -D warnings, nursery group).
§1. Done before ADR-0028 — see handoff-19
This session first finished ADR-0027 (the §2 highlight /
hint wiring + precise WARNING spans + the LIKE-on-numeric
Amendment 1 + the debounce state machine) and fixed two
manual-testing bugs (optional trailing-flag completion after
--; the --resume unpersisted-temp pointer). All of that is
documented in handoff-19 — read it for that context. This
file is about ADR-0028.
§2. ADR-0028 step 1 — done (03d8a09)
ADR-0028's build-order step 1: the styled-output-line
mechanism. OutputLine gained an optional
styled_runs: Option<Vec<OutputSpan>> payload;
render_output_line (src/ui.rs) gained a branch that, when
the payload is present, renders the line span-by-span — each
OutputSpan { byte_range, class }'s semantic
OutputStyleClass (Neutral / Efficient / Expensive /
AutomaticIndex) resolved to a theme colour by
output_span_style. The echo path is untouched; a line with
no payload still renders whole-line by kind.
theme.plan_efficient was added — a green distinct from
theme.system (ADR-0028 §6); theme.warning is reused for
"expensive". OutputLine::styled(...) is the constructor for
payload-bearing lines.
No user-visible change yet — nothing produces styled lines.
Two ui tests cover the new branch and the no-payload
fallback.
§3. ADR-0028 steps 2–5 — the plan
Read docs/adr/0028-query-plans.md first — it is the spec.
Below is the validated build plan with file:line anchors, so
no re-exploration is needed.
Three gotchas found during exploration
Subgrammarneeds&'static Node; the query shapes areconst.SHOW_DATA/UPDATE_SHAPE/DELETE_SHAPE(src/dsl/grammar/data.rs:68,300,311) areconst. Aconstcannot be referenced as&'static; and promoting them tostaticbreaksconst SHOW_CHOICES(data.rs:76), which embedsSHOW_DATAby value. Fix: add three thin wrapperstatics over the existing_NODESslices —static EXPLAIN_SHOW_DATA: Node = Node::Seq(SHOW_DATA_NODES);etc. — and haveexplainreference those. Astaticmay read aconst, and aconstarray may take&of astatic. No churn to the existing shapes.build_showis positional.build_show(data.rs:347) readswords.nth(1)to telldatafromtable— index 1 assumesshowis the entry word at index 0. Underexplainthe indices shift. Fix: extract a role-basedbuild_show_data(path)helper (it only needsrequire_ident(path,"table_name"),build_show_filter,build_show_limit— all already position-independent);build_show'sdataarm calls it.build_update/build_deleteare already role-based, so they need nothing.- Steps 2 and 3 must land in one commit. Adding the
Command::Explainenum variant breaks every exhaustivematch Command— notablyexecute_command_typed(src/runtime.rs:1685). So the grammar/AST (ADR step 2) and the worker path (ADR step 3) cannot be separate green commits. Combine them: one commit landingexplainend-to-end with a plain tree render; step 4 then adds the styled / annotated tree.
Step 2 + 3 (one commit) — explain end to end
Grammar — src/dsl/grammar/data.rs:
- Add
static EXPLAIN_SHOW_DATA / EXPLAIN_UPDATE / EXPLAIN_DELETE = Node::Seq(SHOW_DATA_NODES / UPDATE_NODES / DELETE_NODES). const EXPLAIN_SHOW_NODES = &[Word("show"), Subgrammar(&EXPLAIN_SHOW_DATA)]; likewise update / delete.const EXPLAIN_CHOICES = &[Seq(EXPLAIN_SHOW_NODES), …];const EXPLAIN_SHAPE = Node::Choice(EXPLAIN_CHOICES). The surface isexplain show data …/explain update …/explain delete from …(ADR §1 — the inner entry word is part of the surface).- Extract
build_show_data(path); havebuild_show'sSome("data")arm call it. build_explain(path): the words are[explain, show|update| delete, …];words.nth(1)discriminates → callbuild_show_data/build_update/build_delete, box the result:Command::Explain { query: Box::new(inner) }.pub static EXPLAIN: CommandNode { entry: Word::keyword("explain"), shape: EXPLAIN_SHAPE, ast_builder: build_explain, help_id: None, usage_ids: &["parse.usage.explain"] }.
Registry: add &data::EXPLAIN to REGISTRY
(src/dsl/grammar/mod.rs:466). "explain" is a free entry
word.
Catalog: add parse.usage.explain to
src/friendly/strings/en-US.yaml (the parse: usage:
section) and ("parse.usage.explain", &[]) to
src/friendly/keys.rs KEYS_AND_PLACEHOLDERS (sorted) — the
keys_validate_against_catalog test enforces this.
Command AST — src/dsl/command.rs:
Command::Explain { query: Box<Command> }.verb()(:442, exhaustive) — addSelf::Explain { .. } => "explain".target_table()(:481, exhaustive) — addSelf::Explain { query } => query.target_table().display_subject()(:530) has a_arm — no change needed.
DB worker — src/db.rs:
- New
ExplainRow { id: i64, parent: i64, detail: String }andQueryPlan { display_sql: String, rows: Vec<ExplainRow> }. Request::ExplainPlan { query: Command, reply: oneshot::Sender<Result<QueryPlan, DbError>> }(Requestat:409); dispatch arm inhandle_request.Database::explain_query_plan(query: Command) -> Result<QueryPlan, DbError>async method (mirror the existingoneshotrequest methods).- Separate SQL construction from execution. Extract
build_query_data_sql/build_update_sql/build_delete_sqlreturning(String, Vec<rusqlite Value>)fromdo_query_data(:4730) /do_update(:4539) /do_delete(:4642) — the SQL string + params are already fully assembled before the firstconn.prepare/execute_with_fk_enrichmentin each, so the cut is clean. do_explain_plan(conn, query)— match the inner command, call the shared SQL builder, runEXPLAIN QUERY PLAN <sql>with the params bound (template:read_table_indexes,:3305— the same multi-row read shape), collectExplainRows. Also build the display SQL (ADR §3): standard SQL with double-quoted idents (quote_ident,:1713— make itpub(crate)or duplicate), WHERE literals inline (acompile_operandvariant that renders the literal instead of pushing a?param),<>for inequality (compare_op_sqlalready emits<>), and the implicitORDER BY <pk>thatlimitadds.
Result flow:
CommandOutcome::QueryPlan(QueryPlan)(src/runtime.rs:1490).AppEvent::DslExplainSucceeded { command: Command, plan: QueryPlan }(src/event.rs:17).execute_command_typed(runtime.rs:1685) arm:Command::Explain { query } => database.explain_query_plan(*query).await .map(CommandOutcome::QueryPlan).spawn_dsl_dispatch(runtime.rs:1129) — mapCommandOutcome::QueryPlan→AppEvent::DslExplainSucceeded.App::update— handleDslExplainSucceeded: for this commit render a plain tree (indenteddetailstrings + the display SQL line), pushed asOutputKind::Systemlines. Step 4 swaps in the styled renderer.
Step 4 (one commit) — render_explain_plan + taxonomy
render_explain_planinsrc/output_render.rs(alongsiderender_structure/render_data_table). Builds the box-drawing tree fromid/parent; node text is thedetailstring verbatim; the display SQL is shown above the tree; the command echo precedes it.- Annotation taxonomy (ADR §4): a small substring-pattern
table classifying each
detailinto Full scan / Index search / Covering index / PK lookup / Automatic index / Temp B-tree / Neutral. Unmatched → Neutral. - The renderer emits
OutputLines withstyled_runs: Some(...)— only the category-bearing keywords of eachdetailget anOutputStyleClass(Efficient → green, Expensive → amber, AutomaticIndex → amber + bold + a distinct marker per ADR §6); connectors and names stay Neutral. The mechanism for this is already built (step 1). App::update'sDslExplainSucceededhandler switches from the plain render torender_explain_plan.
Step 5 (one commit) — matrix + verification
- Typing-surface matrix cells for
explainintests/typing_surface/(parse / completion / hint ofexplain show data …etc.). - Full suite green vs. the 1133 baseline; clippy clean; matrix-snapshot discipline.
- Final handoff (handoff-21).
§4. What's next
- ADR-0028 steps 2–5 per §3 —
explainis the only active feature in flight. - After ADR-0028: the handoff-16 design trio is then fully
implemented. Other open clusters unchanged from
handoff-16/17/18/19 (snapshot/undo
U-series; constraintsC3;C4m:n;C3a;C1;H1;SD1;TT5CI;V4;I1;TU1). Prioritisation is a user decision — ask.
§5. How to take over
- Read this file, then
docs/adr/0028-query-plans.md(the spec), then handoff-19 (ADR-0027 + the two bugs). - Read
CLAUDE.md— working-style rules. - Run
cargo test— 1133 passing, 0 failing, 1 ignored. - Run
cargo clippy --all-targets -- -D warnings— clean. - Implement ADR-0028 §3 — step 2+3 as one commit, then step 4, then step 5. The file:line anchors in §3 are from this session's exploration; the three gotchas are the non-obvious parts.
Note on the typing-surface matrix
tests/typing_surface/ is 161 cells. After the explain
grammar lands (step 2+3), add cells for it (step 5) and apply
the matrix-snapshot discipline from handoff-17/18/19: a
failing cell with correct new behaviour → update its
snapshot; with wrong behaviour → the cell earned its keep.