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:
+11
-11
@@ -522,7 +522,7 @@ fn parse_action(words: &[&'static str]) -> ReferentialAction {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_drop(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
fn build_drop(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
// Discriminate by the second word matched (the entry was
|
||||
// `drop`, the next Word is `table` / `column` / `relationship`).
|
||||
let sub = path
|
||||
@@ -566,7 +566,7 @@ fn build_drop(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
})
|
||||
}
|
||||
}
|
||||
Some("constraint") => build_drop_constraint(path),
|
||||
Some("constraint") => build_drop_constraint(path, _source),
|
||||
Some("relationship") => {
|
||||
// Endpoints form has `from` as the third Word.
|
||||
let has_from = path
|
||||
@@ -597,7 +597,7 @@ fn build_drop(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
fn build_add(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
// Second matched Word distinguishes column vs the `1:n
|
||||
// relationship` form. The `1` literal counts as a Word
|
||||
// (the walker records Literal matches as MatchedKind::Word
|
||||
@@ -631,13 +631,13 @@ fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
check,
|
||||
})
|
||||
}
|
||||
Some("1") => build_add_relationship(path),
|
||||
Some("1") => build_add_relationship(path, _source),
|
||||
Some("index") => Ok(Command::AddIndex {
|
||||
name: ident(path, "index_name").map(str::to_string),
|
||||
table: require_ident(path, "table_name")?,
|
||||
columns: collect_idents(path, "column_name"),
|
||||
}),
|
||||
Some("constraint") => build_add_constraint(path),
|
||||
Some("constraint") => build_add_constraint(path, _source),
|
||||
_ => Err(ValidationError {
|
||||
message_key: "parse.error_wrapper",
|
||||
args: vec![("detail", "unknown add subcommand".to_string())],
|
||||
@@ -645,7 +645,7 @@ fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_add_relationship(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
fn build_add_relationship(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
// Collect all referential-clause actions in matched order
|
||||
// and validate at-most-2 + not-repeated. The `on <delete|
|
||||
// update> <action>` sequence shows up as a run of Word
|
||||
@@ -715,7 +715,7 @@ fn build_add_relationship(path: &MatchedPath) -> Result<Command, ValidationError
|
||||
})
|
||||
}
|
||||
|
||||
fn build_rename_column(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
fn build_rename_column(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
Ok(Command::RenameColumn {
|
||||
table: require_ident(path, "table_name")?,
|
||||
old: require_ident(path, "column_name")?,
|
||||
@@ -723,7 +723,7 @@ fn build_rename_column(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_change_column(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
fn build_change_column(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
let ty_text = require_ident(path, "type")?;
|
||||
let ty = ty_text
|
||||
.parse::<crate::dsl::types::Type>()
|
||||
@@ -775,7 +775,7 @@ fn build_change_column(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
/// recovers it. The §9 redundancy and §5 dry-run checks are
|
||||
/// execution-time (the parser has no schema) and live in the
|
||||
/// database worker.
|
||||
fn build_add_constraint(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
fn build_add_constraint(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
let (not_null, unique, default, check) = collect_column_constraints(path)?;
|
||||
let constraint = if not_null {
|
||||
Constraint::NotNull
|
||||
@@ -802,7 +802,7 @@ fn build_add_constraint(path: &MatchedPath) -> Result<Command, ValidationError>
|
||||
/// (ADR-0029 §2.2). `drop` names only the kind — the
|
||||
/// `DROP_CONSTRAINT_KIND` Choice is payload-free, so the kind
|
||||
/// is recovered from which keyword(s) the path matched.
|
||||
fn build_drop_constraint(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
fn build_drop_constraint(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
let words: Vec<&'static str> = path
|
||||
.items
|
||||
.iter()
|
||||
@@ -1113,7 +1113,7 @@ fn redundant_pk_constraint(column: &str, constraint: &str) -> ValidationError {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_create_table(path: &MatchedPath) -> Result<Command, ValidationError> {
|
||||
fn build_create_table(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
let name = require_ident(path, "table_name")?;
|
||||
|
||||
// Walk the matched items, segmenting per column: a
|
||||
|
||||
Reference in New Issue
Block a user