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:
@@ -567,6 +567,19 @@ enum Request {
|
||||
source: Option<String>,
|
||||
reply: oneshot::Sender<Result<DataResult, DbError>>,
|
||||
},
|
||||
/// Run a SQL `SELECT` typed by the user in advanced mode
|
||||
/// (ADR-0030 §6, ADR-0031). The grammar walker has already
|
||||
/// validated `sql` is in the supported subset; the worker
|
||||
/// prepares and runs the statement and returns the rows as
|
||||
/// a [`DataResult`] (with no playground type information per
|
||||
/// ADR-0030 §6 — computed columns render with neutral
|
||||
/// alignment). `source` is the literal submitted line,
|
||||
/// appended to `history.log` for replay (ADR-0030 §11).
|
||||
RunSelect {
|
||||
sql: String,
|
||||
source: Option<String>,
|
||||
reply: oneshot::Sender<Result<DataResult, DbError>>,
|
||||
},
|
||||
/// Capture the query plan for an explainable command via
|
||||
/// `EXPLAIN QUERY PLAN` (ADR-0028 §2). `query` is the inner
|
||||
/// `ShowData` / `Update` / `Delete`; `EXPLAIN QUERY PLAN`
|
||||
@@ -1010,6 +1023,20 @@ impl Database {
|
||||
recv.await.map_err(|_| DbError::WorkerGone)?
|
||||
}
|
||||
|
||||
/// Run a validated SQL `SELECT` and return the rows
|
||||
/// (ADR-0030 §6, ADR-0031). `sql` is the grammar-validated
|
||||
/// statement text; `source` is the literal submitted line
|
||||
/// for `history.log`.
|
||||
pub async fn run_select(
|
||||
&self,
|
||||
sql: String,
|
||||
source: Option<String>,
|
||||
) -> Result<DataResult, DbError> {
|
||||
let (reply, recv) = oneshot::channel();
|
||||
self.send(Request::RunSelect { sql, source, reply }).await?;
|
||||
recv.await.map_err(|_| DbError::WorkerGone)?
|
||||
}
|
||||
|
||||
/// Capture the query plan for an explainable command
|
||||
/// (ADR-0028 §2). The wrapped command is not executed —
|
||||
/// `EXPLAIN QUERY PLAN` only inspects how the engine would
|
||||
@@ -1447,6 +1474,14 @@ fn handle_request(conn: &Connection, persistence: Option<&Persistence>, req: Req
|
||||
limit,
|
||||
));
|
||||
}
|
||||
Request::RunSelect { sql, source, reply } => {
|
||||
let _ = reply.send(do_run_select_request(
|
||||
conn,
|
||||
persistence,
|
||||
source.as_deref(),
|
||||
&sql,
|
||||
));
|
||||
}
|
||||
Request::RebuildFromText {
|
||||
project_path,
|
||||
source,
|
||||
@@ -5645,6 +5680,69 @@ fn do_query_data_request(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Worker handler for `Request::RunSelect` (ADR-0030 §6,
|
||||
/// ADR-0031). Mirrors `do_query_data_request`: run the
|
||||
/// statement, append the literal line to `history.log` so a
|
||||
/// replay re-runs it (ADR-0030 §11).
|
||||
fn do_run_select_request(
|
||||
conn: &Connection,
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
sql: &str,
|
||||
) -> Result<DataResult, DbError> {
|
||||
let data = do_run_select(conn, sql)?;
|
||||
if let (Some(p), Some(text)) = (persistence, source) {
|
||||
p.append_history(text)
|
||||
.map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Execute a grammar-validated SQL `SELECT` and collect its
|
||||
/// rows into a [`DataResult`] (ADR-0030 §6).
|
||||
///
|
||||
/// All result columns are reported with `column_types = None` —
|
||||
/// a SELECT result has no playground type unless we resolve
|
||||
/// each output column back to its source column, which is a
|
||||
/// Phase-2 (full `SELECT`) concern. The DSL data-table renderer
|
||||
/// (ADR-0016) renders typeless columns with neutral alignment.
|
||||
fn do_run_select(conn: &Connection, sql: &str) -> Result<DataResult, DbError> {
|
||||
debug!(sql = %sql, "run_select");
|
||||
let mut stmt = conn.prepare(sql).map_err(DbError::from_rusqlite)?;
|
||||
let column_names: Vec<String> = stmt
|
||||
.column_names()
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
let col_count = column_names.len();
|
||||
let column_types: Vec<Option<Type>> = vec![None; col_count];
|
||||
let rows_iter = stmt
|
||||
.query_map([], |row| {
|
||||
let mut cells: Vec<rusqlite::types::Value> = Vec::with_capacity(col_count);
|
||||
for i in 0..col_count {
|
||||
cells.push(row.get(i)?);
|
||||
}
|
||||
Ok(cells)
|
||||
})
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
let mut rows: Vec<Vec<Option<String>>> = Vec::new();
|
||||
for r in rows_iter {
|
||||
let cells = r.map_err(DbError::from_rusqlite)?;
|
||||
rows.push(
|
||||
cells
|
||||
.into_iter()
|
||||
.map(|v| format_cell(v, None))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
Ok(DataResult {
|
||||
table_name: String::new(),
|
||||
columns: column_names,
|
||||
column_types,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the parameterised `SELECT … FROM …` statement for a
|
||||
/// `show data` query (ADR-0026 §5–§6). Separated from
|
||||
/// `do_query_data` so the `explain` path runs `EXPLAIN QUERY
|
||||
|
||||
Reference in New Issue
Block a user