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:
+67
-29
@@ -990,27 +990,12 @@ impl App {
|
||||
return self.dispatch_app_command(app_cmd, &effective_input);
|
||||
}
|
||||
|
||||
// For everything else: dispatch by effective mode.
|
||||
match effective_mode {
|
||||
Mode::Simple => self.dispatch_dsl(&effective_input, effective_mode),
|
||||
Mode::Advanced => {
|
||||
// SQL handling is not implemented yet; show a placeholder
|
||||
// until the advanced-mode SQL path lands. Once it does,
|
||||
// this branch parses with sqlparser-rs and dispatches
|
||||
// analogously to the DSL path below.
|
||||
self.note_system(crate::t!(
|
||||
"advanced_mode.not_implemented",
|
||||
input = effective_input
|
||||
));
|
||||
self.push_output(OutputLine {
|
||||
text: effective_input,
|
||||
kind: OutputKind::Echo,
|
||||
mode_at_submission: effective_mode,
|
||||
styled_runs: None,
|
||||
});
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
// For everything else: unified dispatch. `dispatch_dsl`
|
||||
// parses with `effective_mode` (ADR-0030 §2), so a SQL
|
||||
// form in advanced mode runs and a SQL form in simple
|
||||
// mode yields the precise "this is SQL" hint through the
|
||||
// walker's mode gate — no separate placeholder branch.
|
||||
self.dispatch_dsl(&effective_input, effective_mode)
|
||||
}
|
||||
|
||||
/// Dispatch a parsed app-lifecycle command. Works in both
|
||||
@@ -1091,7 +1076,16 @@ impl App {
|
||||
// value slots (insert-into-T-values-…) dispatch on the
|
||||
// column's actual user-facing type instead of accepting
|
||||
// any literal at bind time.
|
||||
match crate::dsl::parser::parse_command_with_schema(input, &self.schema_cache) {
|
||||
//
|
||||
// ADR-0030 §2: parse with the submission's effective
|
||||
// mode so the walker gates SQL-only forms — simple-mode
|
||||
// `select` returns the "this is SQL" hint as a normal
|
||||
// parse error and is rendered through the Err arm below.
|
||||
match crate::dsl::parser::parse_command_with_schema_in_mode(
|
||||
input,
|
||||
&self.schema_cache,
|
||||
submission_mode,
|
||||
) {
|
||||
Ok(Command::Replay { path }) => {
|
||||
// `replay` is parsed as a DSL command for the
|
||||
// sake of grammar uniformity, but its execution
|
||||
@@ -1443,6 +1437,10 @@ impl App {
|
||||
C::ShowData { name, .. } | C::ShowTable { name } => {
|
||||
(Operation::Query, Some(name.as_str()), None)
|
||||
}
|
||||
// A SQL `SELECT` carries only its statement text —
|
||||
// no single table name to fall back on. A query
|
||||
// failure routes through `Operation::Query`.
|
||||
C::Select { .. } => (Operation::Query, None, 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.
|
||||
@@ -2270,19 +2268,40 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_in_advanced_mode_echoes_with_advanced_tag() {
|
||||
fn enter_in_advanced_mode_dispatches_select_with_advanced_tag() {
|
||||
// The pre-ADR-0030 placeholder echoed any advanced-mode
|
||||
// input back unexecuted; with the SQL surface live, a
|
||||
// `select` in advanced mode runs through `dispatch_dsl`
|
||||
// exactly like a DSL command, producing the standard
|
||||
// `running: …` echo and an `ExecuteDsl(Command::Select)`
|
||||
// action. The mode-tag invariant — that the echo carries
|
||||
// the submission's effective mode — is what this test
|
||||
// pins down.
|
||||
let mut app = App::new();
|
||||
app.mode = Mode::Advanced;
|
||||
type_str(&mut app, "select 1");
|
||||
submit(&mut app);
|
||||
// We expect a placeholder system line plus the echoed line.
|
||||
let actions = submit(&mut app);
|
||||
let echoed = app
|
||||
.output
|
||||
.iter()
|
||||
.rfind(|l| l.kind == OutputKind::Echo)
|
||||
.unwrap();
|
||||
assert_eq!(echoed.mode_at_submission, Mode::Advanced);
|
||||
assert_eq!(echoed.text, "select 1");
|
||||
assert!(
|
||||
echoed.text.contains("select 1"),
|
||||
"echo line carries the input: {:?}",
|
||||
echoed.text,
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
actions.as_slice(),
|
||||
[Action::ExecuteDsl {
|
||||
command: Command::Select { .. },
|
||||
..
|
||||
}],
|
||||
),
|
||||
"advanced-mode `select` should produce ExecuteDsl(Select); got {actions:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2326,17 +2345,36 @@ mod tests {
|
||||
fn colon_prefix_in_simple_mode_evaluates_as_advanced_one_shot() {
|
||||
let mut app = App::new();
|
||||
type_str(&mut app, ":select 1");
|
||||
submit(&mut app);
|
||||
let actions = submit(&mut app);
|
||||
// The persistent mode is unchanged.
|
||||
assert_eq!(app.mode, Mode::Simple);
|
||||
// The advanced echo line is present.
|
||||
let echoed = app
|
||||
.output
|
||||
.iter()
|
||||
.rfind(|l| l.kind == OutputKind::Echo)
|
||||
.unwrap();
|
||||
// The line ran under the one-shot effective mode, so
|
||||
// the echo carries the Advanced tag…
|
||||
assert_eq!(echoed.mode_at_submission, Mode::Advanced);
|
||||
assert_eq!(echoed.text, "select 1");
|
||||
// …and the `:` is stripped before dispatch (the SQL
|
||||
// executed is `select 1`, not `:select 1`).
|
||||
assert!(
|
||||
echoed.text.contains("select 1") && !echoed.text.contains(":select"),
|
||||
"echo carries the stripped input: {:?}",
|
||||
echoed.text,
|
||||
);
|
||||
// The one-shot dispatched the SELECT through the same
|
||||
// path as a persistent-advanced submission.
|
||||
assert!(
|
||||
matches!(
|
||||
actions.as_slice(),
|
||||
[Action::ExecuteDsl {
|
||||
command: Command::Select { .. },
|
||||
..
|
||||
}],
|
||||
),
|
||||
"`:select 1` should produce ExecuteDsl(Select); got {actions:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user