Implementation handoff: a SQL `select` typed in advanced mode
parses, runs, and renders end to end; the same line in simple
mode lights up the precise "this is SQL" hint instead. ADR-0031
(the SQL expression grammar) and ADR-0030 Phase 1 ("Foundations
+ first SELECT") landed across five commits. Tests 1240 → 1260,
clippy clean.
The handoff records: the walker mode gate + `is_advanced_only`
set, the `ast_builder` source-param sweep, `Command::Select`
carrying the validated SQL text, the `data::SELECT` shape, the
worker `Request::RunSelect` round-trip, the ambient mode
threading through completion / overlay / validity indicator,
the autonomous calls made during execution (FROM optional,
implicit alias unsupported, etc.), and the seams the next
session uses to take up ADR-0030 Phase 2 (full SELECT) — which
gets its own focused ADR before code, per ADR-0030 §3.
16 KiB
Session handoff — 2026-05-19 (25)
Twenty-fifth handover. Implementation session. ADR-0030
Phase 1 ("Foundations + first SELECT") and ADR-0031 (the SQL
expression grammar) landed end to end: a SQL select typed in
advanced mode parses, runs against the database, and renders
through the existing data-table renderer; the same line in
simple mode lights up the precise "this is SQL" hint instead of
running, and Tab completion / live overlay / [ERR] indicator
agree.
§1. State at handoff
Branch: main. Working tree clean.
Tests: 1260 passing, 0 failing, 1 ignored (the
unchanged doctest). Up from the handoff-24 baseline of 1240 by
+20 — 13 sql-expression unit tests, 7 SQL-SELECT integration
tests. Clippy: clean under -D warnings.
New commits since handoff-24 (a049ff9..HEAD):
cd6371a tests: Phase 1 SQL SELECT integration tests
83e0ddc app: mode-threaded completion, overlay, and validity indicator
6369066 grammar: SQL SELECT end-to-end (ADR-0030 Phase 1)
c93f939 grammar: SQL expression grammar fragment (ADR-0031)
81793a3 docs: ADR-0031 — SQL expression grammar
All local; push asynchronously, not blocking.
§2. What was built — the shape now
Read docs/adr/0031-sql-expression-grammar.md for the
grammar spec (Accepted) and docs/adr/0030-advanced-mode-sql- surface.md for the surrounding architecture.
2.1. ADR-0031: the SQL expression grammar
src/dsl/grammar/sql_expr.rs — a parallel fragment to
grammar/expr.rs. Stratified ladder, one named static Node
per precedence tier:
or_expr → and_expr → not_expr → predicate → additive →
multiplicative → unary → primary
Predicate set: cmp_op / LIKE / BETWEEN / IN / IS NULL
(with the ADR-0026 factoring — operand prefix matched once,
infix NOT as an explicit branch). Primary covers literals,
( or_expr ), case_expr (searched + simple), and
name_or_call — the identifier-prefix shared between column
refs and function calls factored into one Ident followed by
an Optional ( call_args ) tail (the same hazard-avoidance
shape predicate_tail uses). count(*) and count(distinct …) both fall out of call_args.
No AST builder — every Phase-1 consumer (SELECT projection,
WHERE) runs validated SQL as text per ADR-0030 §4/§6. The
fragment's output is the matched-terminal MatchedPath (drives
highlight / completion / hints) plus accept/reject. ADR-0031
§2 documents the rationale.
Drop-in: pub static SQL_EXPRESSION: Node = Subgrammar(&SQL_OR_EXPR);.
SQL CommandNode shapes embed it the way DSL shapes embed
expr::EXPRESSION. Recursion goes through Node::Subgrammar
with ADR-0026's MAX_SUBGRAMMAR_DEPTH = 64 cap reused
unchanged — no new walker capability.
2.2. Walker mode gate (ADR-0030 §2)
WalkContextgainsmode: Mode.ModederivesDefault(= Simple, matching the app's startup mode).grammar::is_advanced_onlykeys the advanced-only entry-word set. Phase 1 has one entry:select.- In
walker::walk(), immediately aftercommand_for_entry_wordresolves theCommandNode, the gate short-circuits whenctx.mode == Simple && is_advanced_only(entry)— returning aWalkResultwhose outcome isValidationFailed { error: { key: "advanced_mode.sql_in_simple", … } }. The entry word still highlights as a keyword; the parse-error layer translates the catalog key into the "switch withmode advanced, or prefix the line with:" hint; the validity indicator goes ERROR. parser::parse_command_with_schema_in_mode(and the schemalessparse_command_in_mode) threadmodeintoWalkContext. Oldparse_command*keep delegating withMode::Advanced(most permissive — back-compat for tests + any caller that doesn't care about gating).
2.3. Builder signature sweep (ADR-0031 §2)
CommandNode.ast_builder is now
fn(&MatchedPath, &str) -> Result<Command, ValidationError> —
the walker forwards the parse source. build_select reads it
to put the validated SQL text into Command::Select; the 21
pre-existing builders accept it as _source. The walker's call
site flips from (&path) to (&path, source) in one place.
2.4. Command::Select + execution
Command::Select { sql: String }— every exhaustivematch Commandupdated (verb,target_table,build_translate_context,execute_command_typed,typing_surface_matrix'scommand_kind_label).grammar::data::SELECT— theCommandNode:Expression slots referenceprojection ('*' | <expr>[ as <alias>][, ...]) [ from <Table> ] [ where <sql_expr> ] [ order by <expr>[ asc|desc][, ...] ] [ limit <n> ] [ ; ]sql_expr::SQL_OR_EXPRviaSubgrammar. TheFROMtable-name slot carries areject_internal_tablevalidator that refuses any__rdbms_*reference at parse time (ADR-0030 §6).- Worker:
Request::RunSelect { sql, source, reply }+Database::run_select+do_run_select_request/do_run_select. The latter prepares the statement, collects rows into aDataResultwithcolumn_types: Vec<None>per ADR-0030 §6, and appends the literal source line tohistory.logso replay re-runs it (ADR-0030 §11). - Runtime:
execute_command_typedgains theCommand::Selectarm →database.run_select(sql, src)→CommandOutcome::Query→AppEvent::DslDataSucceeded→render_data_table. Reuses the existingshow datarendering path end to end.
2.5. Dispatch unification + ambient mode threading
App::submitis unified: both modes go throughdispatch_dsl(&effective_input, effective_mode), which parses with the line's effective mode. The placeholder advanced-mode echo branch is gone (and theadvanced_mode.not_implementedcatalog entry is dead code now — left in place; remove on a passing future cleanup).EffectiveMode::as_mode()collapses the persistent / one-shot distinction into the plainModethe walker reads.- Walker exposes
completion_probe_in_mode,expected_at_input_in_mode,input_verdict_in_mode. The empty-input / unknown-entry fallbacks in the first two filter theREGISTRYlisting byis_advanced_only— Tab in simple mode does not offerselect. - Completion exposes
candidates_at_cursor_in_mode/candidates_at_cursor_with_in_mode. Internally they route theparse_commandcompleteness probe and thecompletion_probe/expected_atcalls through the mode-aware variants. App::input_validity_verdictand the Tab handlers (start_or_complete_at/_last) passMode::Simple(validity indicator only fires in plain simple mode) andself.effective_mode().as_mode()(Tab handlers).input_render::render_input_runsandambient_hintare invoked only when effective mode is plainSimple(ui.rs gates), so their internal calls hardcodeMode::Simple.
2.6. Catalog (ADR-0019)
advanced_mode.sql_in_simple{command}— the walker gate hint.select.internal_table{table}— the__rdbms_*rejection.parse.usage.select— the parse-error usage template.
§3. Behaviour now (user-visible)
- Advanced-mode
selectruns:select 1,select * from t,select Name from Customers where Age > 18 order by Name limit 10— all parse, dispatch asCommand::Select, run via the worker, render as a data table. - Simple-mode
selectdoes not run: the walker gates it, the dispatch path renders the catalog hint (advanced_mode.sql_in_simple), the validity indicator lights ERROR before submit, the live overlay marks the input as a definite error, Tab does not offer it as a completion. :select …in simple mode → one-shot Advanced, dispatches the SELECT; persistent mode unchanged.__rdbms_*inFROM→ parse rejected withselect.internal_table.history.logrecords every executed SELECT (literal line);replayre-runs them through the same dispatch path.
The data-table renderer treats SELECT results as a typeless
view — column_types: Vec<None>. Pedagogically that means
neutral alignment for everything; one specific UX note: bool
columns SELECTed render as 0/1 rather than true/false.
Phase 2 can enrich plain-column refs back to their source type.
§4. Autonomous decisions during execution
Per CLAUDE.md, these were judgement calls outside the original approach — they are flagged here so the next session can sanction or override:
FROMclause is optional. ADR-0030 Phase 1 says "single-table SELECT"; standard SQL also admitsselect 1/select upper('x')(zero-table constant or function-call form). Included because it is the canonical learner probe; documented in code at the SELECT shape declaration.- Implicit projection aliasing (
select a x) deliberately unsupported. A bare alias and thefromkeyword are structurally ambiguous; onlyselect a as xis admitted. help_id: Noneon the SELECTCommandNode. Thehelplisting skipsselectfor now — thehelp sqlpage is ADR-0030 Phase 6.- Some walker entries still default to internal
Mode::SimpleviaWalkContext::new()—hint_mode_at_input*,hint_resolution_at_input,input_diagnostics,highlight_runs. Production paths that consume them are only invoked when effective mode is plainSimple(ui.rs gates), so the default is behaviourally correct. Full threading lands when advanced mode grows its own ambient assistance (Phase 2-ish). - SELECT result bool columns render as
0/1. All resultcolumn_typesareNone(ADR-0030 §6 — typeless view). Phase 2 can resolve plain column refs back to their schema type and recover the DSLshow datarendering.
§5. What's next — ADR-0030 Phases 2–6
ADR-0030's plan continues. Each later phase is independently
shippable; the two large grammar slices (Phase 2's full
SELECT; Phase 3's SQL DML expression set if it grows) each
warrant their own focused ADR (precedent: ADR-0026, ADR-0031).
The order in the ADR is the suggested execution order; the
user decides what to take next.
-
Phase 2 —
SELECT(full).JOINs (inner / left / right),GROUP BY/HAVING, aggregate functions, subquery expressions ((SELECT …)as a primary;IN (SELECT …);EXISTS (…)),UNION/INTERSECT/EXCEPT, common table expressions (WITH),LIMIT … OFFSET, qualified column refs (t.c,alias.c). Its own focused ADR before authoring — same way ADR-0031 preceded the fragment. The expression grammar (ADR-0031) is shaped to receive the subquery and qualified-ref branches additively; no restructuring foreseen there. -
Phase 3 — DML. SQL
INSERT(single- and multi-row),UPDATE,DELETE. Execute as validated SQL (ADR-0030 §4) via the sameCommand::<X> { sql }-as-text pattern asCommand::Select— the worker's "run validated DML + re- persist the table" request is new. Settle multi-rowINSERTandshortidauto-fill on a SQLINSERT(open question, escalate to the user when reached). -
Phase 4 — DDL. SQL
CREATE/DROP/ALTER TABLE,CREATE/DROP INDEX. DDL routes through the typedCommandexecutor (ADR-0030 §4) — theCommandNode'sast_buildertranslates SQL →Command::{CreateTable, AddColumn, …}, then the existing executor keeps metadata, the type vocabulary (ADR-0005) andSTRICTintact. ADR-0030 §5 maps standard SQL type names (integer,varchar(255),numeric, …) onto the ten-type vocabulary.CHECK/DEFAULTexpressions reuse the ADR-0031 fragment but their storage is text (ADR-0030 §11) — theConstraint::Checkshape may need a text-carrying variant; escalate when reached. Table-rename (C1) may fall out here. -
Phase 5 — DSL → SQL teaching echo. A DSL command run in advanced mode prints its equivalent SQL beneath the
[ok]summary (ADR-0030 §10). It is aCommand→ SQL renderer; uses theOutputLinestyled-runs mechanism (ADR-0028). App-level commands have no SQL form and are not echoed. -
Phase 6 — Polish.
help sql; engine-neutral error sweep (ADR-0030 §7); typing-surface / matrix coverage for the SQL surface; theDOC1SQL-surface reference page.
§6. Seams for the next implementer
sql_expr.rsis the grammar evolution point. Phase 2's subqueryprimarybranch and qualified-column-ref tail are additive — a newChoicebranch inprimary, recursing throughSubgrammarinto the SELECT fragment, and a[ '.' identifier ]tail onname_or_call's ident. The ADR-0031 §7 OOS list calls these out by name.grammar/data.rsSELECT is the model for SQL DML/DDL CommandNodes (Phases 3-4) — sameSubgrammar(&SQL_OR_EXPR)for expression slots, samereject_internal_tablestyle validator on table-name slots, same_NODES/_SHAPE/CommandNodelayout.Command::<X> { sql: String }pattern is the Phase-3 template for DML —build_<x>(_path, source)packages the validated text; the runtime arm sends it to a new "run + re-persist" worker request.WalkContext.modeis the gate; per-Choice-branch tagging (ADR-0030 §2 end state) is what arrives with the shared DSL/SQL entry words (create,insert, …) in Phase 3-4. The whole-command gating viais_advanced_onlycovers Phase 1 cleanly; per-branch tagging is a new mechanism on top — design it when the first shared entry word lands.Database::run_selectis the read-onlyconn.prepare+ collect-rows pattern. The Phase-3 DML "run validated SQL + re- persist the affected table" worker request reuses theconn.preparehalf and adds re-persist (the existingdo_query_data_requestpersistence call is a model).ast_buildernow receivessource: &struniformly — SQL DML/DDL builders in Phases 3-4 use it freely; existing DSL builders ignore it as_source.parse_command*_in_modeis the right entry for any new schema/mode-sensitive call site; the unmodedparse_command*default toMode::Advancedfor back-compat. New production call sites should always pass an explicit mode.
§7. How to take over
- Read this file, then
docs/adr/0030-advanced-mode-sql- surface.mdanddocs/adr/0031-sql-expression-grammar.md. ThenCLAUDE.md(working-style rules) anddocs/ requirements.md(Q1/Q2 partially satisfied; Q4 ticked). cargo test— 1260 passing, 0 failed, 1 ignored.cargo clippy --all-targets -- -D warnings— clean.- Pick the next phase with the user. Phase 2 (full SELECT) is the natural next step and gets its own ADR before code, per ADR-0030 §3 — write the focused grammar ADR first (ADR-0026 / ADR-0031 are the model).
- Escalate, do not guess, on the open per-phase questions
ADR-0030 left — multi-row
INSERT,shortidon SQLINSERT, theConstraint::Checktext-carrying variant, any walker-taxonomy extension that changes the walker's contract.
§8. What else is open
Unchanged from handoff-24 §7 (prioritisation is a user
decision): snapshot/undo U-series (ADR-0006 written,
unbuilt); m:n convenience C4; modify-relationship C3a; the
show family V5; rename-table C1 (may fall out of ADR-0030
Phase 4); friendly-error sweep H1; CI TT5; session-log /
Markdown export V4.
Phase 1's specific deferrals (§4 of this file) — help_id: None
on SELECT (Phase 6), the few walker entries still defaulting to
internal Simple mode, bool rendering in SELECT results — are
documented; the user decides whether to revisit any in Phase 2
or leave until Phase 6 polish.