diff --git a/src/app.rs b/src/app.rs index 98b4fc8..283cd55 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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(); diff --git a/src/dsl/grammar/data.rs b/src/dsl/grammar/data.rs index 665ba50..6441e24 100644 --- a/src/dsl/grammar/data.rs +++ b/src/dsl/grammar/data.rs @@ -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: &[], }; diff --git a/src/dsl/grammar/ddl.rs b/src/dsl/grammar/ddl.rs index d1de91c..83839d8 100644 --- a/src/dsl/grammar/ddl.rs +++ b/src/dsl/grammar/ddl.rs @@ -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"], }; diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs index 1bcdc36..476e060 100644 --- a/src/dsl/grammar/mod.rs +++ b/src/dsl/grammar/mod.rs @@ -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); } diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index 490e7eb..e08ced5 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -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"], diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index 5bf98ed..81f1be5 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -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: