feat(hint): H2 Phase C batch 4 — advanced-mode SQL tier-3 hints (ADR-0053)
Distinct SQL-syntax hints for the 11 advanced-mode forms: sql create table / alter table / create index / drop index / drop table / insert / update / delete, select, with, explain. hint_ids wired on all 11 nodes. Hardened hint_key_for_input_in_mode for shared entry words: a bare multi-form entry word defers to tier-2; when the second token isn't a form word (insert into / update … set), it falls back to the mode-primary key — so advanced mode resolves to the SQL form and simple mode to the DSL form. catalogue + keys.rs registered. +2 spot tests + grammar mode-disambiguation asserts; 2495 pass / 1 ignored, clippy clean.
This commit is contained in:
+21
@@ -5901,6 +5901,27 @@ mod tests {
|
||||
assert!(!output_contains(&app, "rows stored in a table"));
|
||||
}
|
||||
|
||||
// ── Phase C batch 4: advanced-SQL hints (mode-aware) ────────
|
||||
|
||||
#[test]
|
||||
fn f1_in_advanced_mode_renders_the_sql_insert_hint() {
|
||||
let mut app = App::new();
|
||||
app.mode = Mode::Advanced;
|
||||
type_str(&mut app, "insert into Customers (name) values ('x')");
|
||||
f1(&mut app);
|
||||
assert!(output_contains(&app, "Insert rows with SQL"));
|
||||
assert!(!output_contains(&app, "Add one or more rows"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f1_on_select_renders_the_select_hint() {
|
||||
let mut app = App::new();
|
||||
app.mode = Mode::Advanced;
|
||||
type_str(&mut app, "select name from Customers");
|
||||
f1(&mut app);
|
||||
assert!(output_contains(&app, "heart of SQL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_command_toggles_verbosity_and_reports() {
|
||||
let mut app = App::new();
|
||||
|
||||
@@ -1874,7 +1874,7 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode {
|
||||
// too). Mirrors the `SQL_INSERT`/`SQL_UPDATE`/`SQL_DELETE`
|
||||
// precedent; otherwise `note_help` would print `explain` twice.
|
||||
help_id: None,
|
||||
hint_ids: &[],
|
||||
hint_ids: &["explain_sql"],
|
||||
usage_ids: &[],};
|
||||
|
||||
/// SQL `SELECT` (ADR-0030 §6, ADR-0031, ADR-0032).
|
||||
@@ -1890,7 +1890,7 @@ pub static SELECT: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_select::SQL_SELECT_TAIL),
|
||||
ast_builder: build_select,
|
||||
help_id: None,
|
||||
hint_ids: &[],
|
||||
hint_ids: &["select"],
|
||||
usage_ids: &["parse.usage.select"],};
|
||||
|
||||
/// `WITH …` top-level statement (ADR-0032 §4 / sub-phase 2c).
|
||||
@@ -1905,7 +1905,7 @@ pub static WITH: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_select::SQL_WITH_TAIL),
|
||||
ast_builder: build_select,
|
||||
help_id: None,
|
||||
hint_ids: &[],
|
||||
hint_ids: &["with"],
|
||||
usage_ids: &["parse.usage.with"],};
|
||||
|
||||
/// SQL `INSERT` — the `Advanced`-category node of the shared
|
||||
@@ -1923,7 +1923,7 @@ pub static SQL_INSERT: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE),
|
||||
ast_builder: build_sql_insert,
|
||||
help_id: None,
|
||||
hint_ids: &[],
|
||||
hint_ids: &["sql_insert"],
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
@@ -1937,7 +1937,7 @@ pub static SQL_UPDATE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_update::SQL_UPDATE_SHAPE),
|
||||
ast_builder: build_sql_update,
|
||||
help_id: None,
|
||||
hint_ids: &[],
|
||||
hint_ids: &["sql_update"],
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
@@ -1953,7 +1953,7 @@ pub static SQL_DELETE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_delete::SQL_DELETE_SHAPE),
|
||||
ast_builder: build_sql_delete,
|
||||
help_id: None,
|
||||
hint_ids: &[],
|
||||
hint_ids: &["sql_delete"],
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
|
||||
@@ -1879,7 +1879,7 @@ pub static SQL_CREATE_TABLE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&super::sql_create_table::SQL_CREATE_TABLE_SHAPE),
|
||||
ast_builder: build_sql_create_table,
|
||||
help_id: Some("ddl.sql_create_table"),
|
||||
hint_ids: &[],
|
||||
hint_ids: &["sql_create_table"],
|
||||
usage_ids: &["parse.usage.sql_create_table"],
|
||||
};
|
||||
|
||||
@@ -1899,7 +1899,7 @@ pub static SQL_DROP_TABLE: CommandNode = CommandNode {
|
||||
shape: SQL_DROP_TABLE_SHAPE,
|
||||
ast_builder: build_sql_drop_table,
|
||||
help_id: Some("ddl.sql_drop_table"),
|
||||
hint_ids: &[],
|
||||
hint_ids: &["sql_drop_table"],
|
||||
usage_ids: &["parse.usage.sql_drop_table"],
|
||||
};
|
||||
|
||||
@@ -1919,7 +1919,7 @@ pub static SQL_DROP_INDEX: CommandNode = CommandNode {
|
||||
shape: SQL_DROP_INDEX_SHAPE,
|
||||
ast_builder: build_sql_drop_index,
|
||||
help_id: Some("ddl.sql_drop_index"),
|
||||
hint_ids: &[],
|
||||
hint_ids: &["sql_drop_index"],
|
||||
usage_ids: &["parse.usage.sql_drop_index"],
|
||||
};
|
||||
|
||||
@@ -2001,7 +2001,7 @@ pub static SQL_CREATE_INDEX: CommandNode = CommandNode {
|
||||
shape: SQL_CREATE_INDEX_SHAPE,
|
||||
ast_builder: build_sql_create_index,
|
||||
help_id: Some("ddl.sql_create_index"),
|
||||
hint_ids: &[],
|
||||
hint_ids: &["sql_create_index"],
|
||||
usage_ids: &["parse.usage.sql_create_index"],
|
||||
};
|
||||
|
||||
@@ -2560,7 +2560,7 @@ pub static SQL_ALTER_TABLE: CommandNode = CommandNode {
|
||||
shape: SQL_ALTER_TABLE_SHAPE,
|
||||
ast_builder: build_sql_alter_table,
|
||||
help_id: Some("ddl.sql_alter_table"),
|
||||
hint_ids: &[],
|
||||
hint_ids: &["sql_alter_table"],
|
||||
usage_ids: &["parse.usage.sql_alter_table"],
|
||||
};
|
||||
|
||||
|
||||
+38
-1
@@ -616,10 +616,13 @@ pub fn usage_keys_for_input_in_mode(
|
||||
/// form has no tier-3 block yet (the caller falls back to tier-2).
|
||||
#[must_use]
|
||||
pub fn hint_key_for_input_in_mode(source: &str, mode: crate::mode::Mode) -> Option<&'static str> {
|
||||
use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
let nodes = selected_nodes_for_input_in_mode(source, mode);
|
||||
if nodes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Mode-ordered union (advanced-primary first in advanced mode), so a
|
||||
// shared entry word resolves to the surface the user is in.
|
||||
let mut keys: Vec<&'static str> = Vec::new();
|
||||
for (_, node, _) in &nodes {
|
||||
for k in node.hint_ids {
|
||||
@@ -628,7 +631,25 @@ pub fn hint_key_for_input_in_mode(source: &str, mode: crate::mode::Mode) -> Opti
|
||||
}
|
||||
}
|
||||
}
|
||||
pick_form_key(source, &keys)
|
||||
if keys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if keys.len() == 1 {
|
||||
return Some(keys[0]);
|
||||
}
|
||||
// A bare multi-form entry word (no form word yet — `add`⏎) has no
|
||||
// chosen form: defer to tier-2, which lists the choices.
|
||||
let start = skip_whitespace(source, 0);
|
||||
if let Some((_, entry_end)) = consume_ident(source, start)
|
||||
&& skip_whitespace(source, entry_end) >= source.len()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// A form word picks the form (`drop column` → `drop_column`); when
|
||||
// the second token isn't a form word (`insert into …`, `update …
|
||||
// set`), fall back to the mode-primary key — in advanced mode the
|
||||
// SQL form, in simple mode the DSL form.
|
||||
pick_form_key(source, &keys).or_else(|| keys.first().copied())
|
||||
}
|
||||
|
||||
/// Shared mode-aware command-form selection for the entry word at the
|
||||
@@ -922,6 +943,22 @@ mod hint_key_tests {
|
||||
hint_key_for_input_in_mode("drop table T", Mode::Simple),
|
||||
Some("drop_table")
|
||||
);
|
||||
// Mode picks the surface for a shared entry word whose second
|
||||
// token isn't a form word: SQL form in advanced, DSL in simple.
|
||||
assert_eq!(
|
||||
hint_key_for_input_in_mode("insert into T values (1)", Mode::Advanced),
|
||||
Some("sql_insert")
|
||||
);
|
||||
assert_eq!(
|
||||
hint_key_for_input_in_mode("insert into T values (1)", Mode::Simple),
|
||||
Some("insert")
|
||||
);
|
||||
// `create table` shares a form word — advanced-first ordering
|
||||
// resolves it to the SQL form in advanced mode.
|
||||
assert_eq!(
|
||||
hint_key_for_input_in_mode("create table T (id int)", Mode::Advanced),
|
||||
Some("sql_create_table")
|
||||
);
|
||||
// Unknown entry word → None (tier-2 fallback).
|
||||
assert_eq!(hint_key_for_input_in_mode("zzz", Mode::Simple), None);
|
||||
}
|
||||
|
||||
@@ -336,6 +336,40 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
||||
("hint.cmd.replay.what", &[]),
|
||||
("hint.cmd.replay.example", &[]),
|
||||
("hint.cmd.replay.concept", &[]),
|
||||
// Phase C batch 4 — advanced-mode SQL command hints.
|
||||
("hint.cmd.sql_create_table.what", &[]),
|
||||
("hint.cmd.sql_create_table.example", &[]),
|
||||
("hint.cmd.sql_create_table.concept", &[]),
|
||||
("hint.cmd.sql_alter_table.what", &[]),
|
||||
("hint.cmd.sql_alter_table.example", &[]),
|
||||
("hint.cmd.sql_alter_table.concept", &[]),
|
||||
("hint.cmd.sql_create_index.what", &[]),
|
||||
("hint.cmd.sql_create_index.example", &[]),
|
||||
("hint.cmd.sql_create_index.concept", &[]),
|
||||
("hint.cmd.sql_drop_index.what", &[]),
|
||||
("hint.cmd.sql_drop_index.example", &[]),
|
||||
("hint.cmd.sql_drop_index.concept", &[]),
|
||||
("hint.cmd.sql_drop_table.what", &[]),
|
||||
("hint.cmd.sql_drop_table.example", &[]),
|
||||
("hint.cmd.sql_drop_table.concept", &[]),
|
||||
("hint.cmd.sql_insert.what", &[]),
|
||||
("hint.cmd.sql_insert.example", &[]),
|
||||
("hint.cmd.sql_insert.concept", &[]),
|
||||
("hint.cmd.sql_update.what", &[]),
|
||||
("hint.cmd.sql_update.example", &[]),
|
||||
("hint.cmd.sql_update.concept", &[]),
|
||||
("hint.cmd.sql_delete.what", &[]),
|
||||
("hint.cmd.sql_delete.example", &[]),
|
||||
("hint.cmd.sql_delete.concept", &[]),
|
||||
("hint.cmd.select.what", &[]),
|
||||
("hint.cmd.select.example", &[]),
|
||||
("hint.cmd.select.concept", &[]),
|
||||
("hint.cmd.with.what", &[]),
|
||||
("hint.cmd.with.example", &[]),
|
||||
("hint.cmd.with.concept", &[]),
|
||||
("hint.cmd.explain_sql.what", &[]),
|
||||
("hint.cmd.explain_sql.example", &[]),
|
||||
("hint.cmd.explain_sql.concept", &[]),
|
||||
(
|
||||
"hint.ambient_invalid_ident",
|
||||
&["kind", "found"],
|
||||
|
||||
@@ -545,6 +545,52 @@ hint:
|
||||
what: "Re-run the commands recorded in a history file."
|
||||
example: "replay session.log"
|
||||
concept: "Every successful command is journalled, so replaying re-applies them in order to reproduce a project's state — handy for scripting or redoing a sequence."
|
||||
# Advanced-mode SQL forms (Phase C batch 4). Examples are SQL, the
|
||||
# advanced surface — distinct from their simple-mode siblings.
|
||||
sql_create_table:
|
||||
what: "Create a table using SQL syntax (advanced mode)."
|
||||
example: "create table Customers (id int primary key, name text, email text)"
|
||||
concept: "Advanced mode speaks SQL: constraints go inline (`primary key`, `not null`, `unique`, `check`). This is the raw form of simple mode's `create table … with pk …`."
|
||||
sql_alter_table:
|
||||
what: "Change a table's structure with SQL `alter table` (advanced mode)."
|
||||
example: "alter table Customers add column phone text"
|
||||
concept: "`alter table` adds or drops columns, renames, and adds constraints — the SQL equivalent of simple mode's `add column` / `drop column` / `change column`."
|
||||
sql_create_index:
|
||||
what: "Create an index with SQL (advanced mode)."
|
||||
example: "create index ix_email on Customers (email)"
|
||||
concept: "Add `unique` to also forbid duplicate values. The simple-mode equivalent is `add index`."
|
||||
sql_drop_index:
|
||||
what: "Remove an index with SQL (advanced mode)."
|
||||
example: "drop index ix_email"
|
||||
concept: "Only the lookup shortcut goes; the data is untouched. Add `if exists` to ignore a missing index."
|
||||
sql_drop_table:
|
||||
what: "Remove a table with SQL (advanced mode)."
|
||||
example: "drop table Customers"
|
||||
concept: "Add `if exists` to avoid an error when the table might not be there. Relationships pointing at it may block the drop."
|
||||
sql_insert:
|
||||
what: "Insert rows with SQL (advanced mode)."
|
||||
example: "insert into Customers (name, email) values ('Ann', 'ann@example.io')"
|
||||
concept: "Naming the columns lets you supply them in any order and skip ones that have a default — the SQL form of simple mode's `insert`."
|
||||
sql_update:
|
||||
what: "Update rows with SQL (advanced mode)."
|
||||
example: "update Customers set email = 'new@example.io' where id = 1"
|
||||
concept: "`set` lists the new values; `where` picks which rows change. The SQL form of simple mode's `update`."
|
||||
sql_delete:
|
||||
what: "Delete rows with SQL (advanced mode)."
|
||||
example: "delete from Orders where status = 'cancelled'"
|
||||
concept: "`where` picks the rows to remove; foreign-key rules still apply. The SQL form of simple mode's `delete`."
|
||||
select:
|
||||
what: "Query rows with SQL `select` (advanced mode)."
|
||||
example: "select name, email from Customers where id = 1"
|
||||
concept: "`select` is read-only: choose columns (or `*`), filter with `where`, sort with `order by`, cap with `limit`. This is the heart of SQL — and the reason advanced mode exists."
|
||||
with:
|
||||
what: "Name a sub-query (a CTE) and read from it in a `select` (advanced mode)."
|
||||
example: "with recent as (select * from Orders where id > 100) select * from recent"
|
||||
concept: "A `with` clause (Common Table Expression) names a query so the main `select` can use it like a temporary table — handy for breaking a complex query into readable steps."
|
||||
explain_sql:
|
||||
what: "Show how the database will run a SQL query, without running it (advanced mode)."
|
||||
example: "explain select * from Customers where email = 'a@example.io'"
|
||||
concept: "Like simple mode's `explain`, but wraps a raw SQL statement. It reveals whether an index is used, and never executes."
|
||||
err:
|
||||
foreign_key:
|
||||
child_side:
|
||||
|
||||
Reference in New Issue
Block a user