grammar: SQL SELECT end-to-end (ADR-0030 Phase 1)

The first cut of advanced-mode SQL: a `select` line in advanced
mode parses, runs against the database, and renders its rows
through the existing data-table renderer; the same line in
simple mode lights up the precise "this is SQL" hint instead of
running.

Walker mode gate (ADR-0030 §2)
------------------------------
- `WalkContext` gains a `mode: Mode` field; `Mode` derives
  `Default` (= `Simple`, matching the app's startup mode).
- `grammar::is_advanced_only` keys an advanced-only entry-word
  set (Phase 1: just `select`). When the walker matches an
  advanced-only entry word with `ctx.mode == Simple`, it
  short-circuits to a `WalkOutcome::ValidationFailed` carrying
  the `advanced_mode.sql_in_simple` catalog key — the input
  highlights as a keyword, the validity indicator goes ERROR,
  and the parse-error layer renders the "switch with `mode
  advanced`, or prefix the line with `:`" hint.
- `parser::parse_command_with_schema_in_mode` (and the
  schemaless `parse_command_in_mode`) threads the mode into
  `WalkContext`; existing `parse_command*` entry points default
  to `Mode::Advanced` (most permissive) so back-compat callers
  see the full grammar.
- `App::submit` is unified: both modes route through
  `dispatch_dsl(&effective_input, effective_mode)`, which now
  parses with the line's effective mode. The placeholder
  advanced-mode echo branch is gone.

Builder signature sweep (ADR-0031 §2)
-------------------------------------
- `CommandNode.ast_builder` gains a `source: &str` parameter,
  forwarded by the walker. `build_select` reads it to put the
  validated SQL text into `Command::Select`; the 21 existing
  builders accept it as `_source`.

SQL `SELECT` (ADR-0030 §6, ADR-0031)
-------------------------------------
- New `Command::Select { sql: String }` variant. Every
  exhaustive `match Command` updated (`verb`, `target_table`,
  `build_translate_context`, `execute_command_typed`,
  `typing_surface`'s label).
- `grammar::data::SELECT` `CommandNode`: projection (`*` or
  `expr [as alias]` list), optional `FROM <table>`, optional
  `WHERE`/`ORDER BY`/`LIMIT`, optional trailing `;`. The
  expression slots reference the ADR-0031 fragment through
  `Subgrammar(&sql_expr::SQL_OR_EXPR)`. The `FROM` table-name
  slot carries a `reject_internal_table` validator that
  refuses `__rdbms_*` references at parse time.
- The `FROM` clause is optional — `select 1`, `select upper('x')`
  (zero-table constant/function-call SELECTs) work alongside
  the single-table form. Standard SQL admits them and they are
  the canonical learner probe.
- Implicit projection aliasing (`select a x`) is deliberately
  unsupported — `from` is a keyword, the bare alias would be
  ambiguous; only `select a as x` is admitted.

Worker / runtime
----------------
- `Request::RunSelect { sql, source, reply }` + a new
  `Database::run_select` method. `do_run_select_request` runs
  the prepared statement, collects rows into a `DataResult`
  with `column_types: Vec<None>` (Phase-1 SELECT result columns
  carry no playground type per ADR-0030 §6), and appends the
  literal source line to `history.log` so replay re-runs it
  (ADR-0030 §11).
- `runtime::execute_command_typed` gains a `Command::Select`
  arm that calls `database.run_select(sql, src)` and maps to
  `CommandOutcome::Query`, which flows into the existing
  `AppEvent::DslDataSucceeded` → `render_data_table` path.

Catalog (ADR-0019)
------------------
- `advanced_mode.sql_in_simple` — the walker's gate message.
- `select.internal_table` — the `__rdbms_*` rejection.
- `parse.usage.select` — the parse-error usage template.

Tests
-----
Two `app::tests` cases that pinned the pre-ADR-0030 placeholder
echo are updated to pin the new dispatch contract — both verify
that the advanced-mode `select` (one persistent, one via the
`:` one-shot) produces `ExecuteDsl(Command::Select)` with the
submission's effective mode tagged on the echo. The matching
walking-skeleton test is updated likewise.

A separate follow-up commit lands the ambient mode-threading
(completion / live overlay / validity indicator) so simple-mode
users do not see SQL surfaced through Tab or the live error
overlay either — the dispatch-layer gate landed here is the
behavioural foundation that follow-up builds on. Integration
tests for the full end-to-end land in a third commit.
This commit is contained in:
claude@clouddev1
2026-05-19 21:46:56 +00:00
parent c93f9394f5
commit 6369066fe4
16 changed files with 527 additions and 71 deletions
+40 -3
View File
@@ -13,6 +13,7 @@
//! identifier-shape token isn't a registered entry word.
use crate::dsl::command::Command;
use crate::mode::Mode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
@@ -94,8 +95,13 @@ impl ParseError {
/// `DynamicSubgrammar`) fall back to schema-unaware behaviour.
/// Use `parse_command_with_schema` to enable typed value slots
/// (ADR-0024 §Phase D).
///
/// Defaults to **advanced**-mode grammar (the full surface) —
/// callers that need simple-mode gating (ADR-0030 §2) use
/// [`parse_command_in_mode`] or
/// [`parse_command_with_schema_in_mode`] instead.
pub fn parse_command(input: &str) -> Result<Command, ParseError> {
parse_command_inner(input, None)
parse_command_inner(input, None, Mode::Advanced)
}
/// Schema-aware parse entry point (ADR-0024 §Phase D).
@@ -104,21 +110,50 @@ pub fn parse_command(input: &str) -> Result<Command, ParseError> {
/// the walker can populate `current_table` / `current_column`
/// from existing entities and `DynamicSubgrammar` factories
/// can unfold per-column typed value slots.
///
/// Defaults to **advanced**-mode grammar; for simple-mode
/// gating use [`parse_command_with_schema_in_mode`].
pub fn parse_command_with_schema(
input: &str,
schema: &crate::completion::SchemaCache,
) -> Result<Command, ParseError> {
parse_command_inner(input, Some(schema))
parse_command_inner(input, Some(schema), Mode::Advanced)
}
/// Schemaless, mode-aware parse (ADR-0030 §2). In `Mode::Simple`
/// the walker gates SQL-only commands and produces the
/// "this is SQL" hint instead of executing them.
pub fn parse_command_in_mode(
input: &str,
mode: Mode,
) -> Result<Command, ParseError> {
parse_command_inner(input, None, mode)
}
/// Schema-aware, mode-aware parse.
///
/// Combines ADR-0024 §Phase D (schema-aware typed slots) with
/// ADR-0030 §2 (mode-gated SQL grammar). The execution path
/// (`App::dispatch_dsl`) and the live overlay / completion /
/// highlight call sites use this so simple-mode users do not
/// see advanced-mode SQL surfaced.
pub fn parse_command_with_schema_in_mode(
input: &str,
schema: &crate::completion::SchemaCache,
mode: Mode,
) -> Result<Command, ParseError> {
parse_command_inner(input, Some(schema), mode)
}
fn parse_command_inner(
input: &str,
schema: Option<&crate::completion::SchemaCache>,
mode: Mode,
) -> Result<Command, ParseError> {
if input.trim().is_empty() {
return Err(ParseError::Empty);
}
if let Some(result) = try_walker_route(input, schema) {
if let Some(result) = try_walker_route(input, schema, mode) {
return result;
}
Err(unknown_command_error(input))
@@ -157,11 +192,13 @@ fn unknown_command_error(source: &str) -> ParseError {
fn try_walker_route(
source: &str,
schema: Option<&crate::completion::SchemaCache>,
mode: Mode,
) -> Option<Result<Command, ParseError>> {
use crate::dsl::walker::{self, outcome::WalkBound};
let mut ctx = schema.map_or_else(walker::context::WalkContext::new, |s| {
walker::context::WalkContext::with_schema(s)
});
ctx.mode = mode;
let (result, command) = walker::walk(source, WalkBound::EndOfInput, &mut ctx);
let result = result?;
Some(walker_outcome_to_parse_result(source, result, command))